pax_global_header00006660000000000000000000000064146125054500014514gustar00rootroot0000000000000052 comment=2740064d97d10676c339be66d18b9a417ef46957 webargs-8.5.0/000077500000000000000000000000001461250545000131605ustar00rootroot00000000000000webargs-8.5.0/.github/000077500000000000000000000000001461250545000145205ustar00rootroot00000000000000webargs-8.5.0/.github/FUNDING.yml000066400000000000000000000000701461250545000163320ustar00rootroot00000000000000open_collective: "marshmallow" tidelift: "pypi/webargs" webargs-8.5.0/.github/dependabot.yml000066400000000000000000000003311461250545000173450ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: pip directory: "/" schedule: interval: daily open-pull-requests-limit: 10 - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" webargs-8.5.0/.github/workflows/000077500000000000000000000000001461250545000165555ustar00rootroot00000000000000webargs-8.5.0/.github/workflows/build-release.yml000066400000000000000000000044621461250545000220230ustar00rootroot00000000000000name: build on: push: branches: ["dev", "*.x-line"] tags: ["*"] pull_request: jobs: tests: name: ${{ matrix.name }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - { name: "3.8", python: "3.8", tox: py38-marshmallow3 } - { name: "3.12", python: "3.12", tox: py312-marshmallow3 } - { name: "lowest", python: "3.8", tox: py38-lowest } - { name: "dev", python: "3.12", tox: py312-marshmallowdev } steps: - uses: actions/checkout@v4.0.0 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - run: python -m pip install tox - run: python -m tox -e${{ matrix.tox }} build: name: Build package runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.11" - name: Install pypa/build run: python -m pip install build - name: Build a binary wheel and a source tarball run: python -m build - name: Install twine run: python -m pip install twine - name: Check build run: python -m twine check --strict dist/* - name: Store the distribution packages uses: actions/upload-artifact@v4 with: name: python-package-distributions path: dist/ # this duplicates pre-commit.ci, so only run it on tags # it guarantees that linting is passing prior to a release lint-pre-release: if: startsWith(github.ref, 'refs/tags') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4.0.0 - uses: actions/setup-python@v5 with: python-version: "3.11" - run: python -m pip install tox - run: python -m tox -e lint publish-to-pypi: name: PyPI release if: startsWith(github.ref, 'refs/tags/') needs: [build, tests, lint-pre-release] runs-on: ubuntu-latest environment: name: pypi url: https://pypi.org/p/webargs permissions: id-token: write steps: - name: Download all the dists uses: actions/download-artifact@v4 with: name: python-package-distributions path: dist/ - name: Publish distribution to PyPI uses: pypa/gh-action-pypi-publish@release/v1 webargs-8.5.0/.gitignore000066400000000000000000000022771461250545000151600ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ README.html # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ webargs-8.5.0/.pre-commit-config.yaml000066400000000000000000000013211461250545000174360ustar00rootroot00000000000000repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.4.1 hooks: - id: ruff - id: ruff-format - repo: https://github.com/python-jsonschema/check-jsonschema rev: 0.28.2 hooks: - id: check-github-workflows - id: check-readthedocs - repo: https://github.com/asottile/blacken-docs rev: 1.16.0 hooks: - id: blacken-docs additional_dependencies: [black==23.12.1] - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.9.0 hooks: - id: mypy language_version: python3 files: ^src/webargs/ additional_dependencies: - marshmallow>=3,<4 - packaging - flask # mypy runs under tox in GitHub Actions, skip it in pre-commit.ci ci: skip: [mypy] webargs-8.5.0/.readthedocs.yml000066400000000000000000000003241461250545000162450ustar00rootroot00000000000000version: 2 sphinx: configuration: docs/conf.py formats: - pdf build: os: ubuntu-22.04 tools: python: "3.11" python: install: - method: pip path: . extra_requirements: - docs webargs-8.5.0/AUTHORS.rst000066400000000000000000000053751461250545000150510ustar00rootroot00000000000000======= Authors ======= Lead ---- * Steven Loria `@sloria `_ * Jérôme Lafréchoux `@lafrech `_ * Stephen Rosen `@sirosen `_ Contributors (chronological) ---------------------------- * Steven Manuatu `@venuatu `_ * Javier Santacruz `@jvrsantacruz `_ * Josh Carp `@jmcarp `_ * `@philtay `_ * Andriy Yurchuk `@Ch00k `_ * Stas Sușcov `@stas `_ * Josh Johnston `@Trii `_ * Rory Hart `@hartror `_ * Jace Browning `@jacebrowning `_ * marcellarius `@marcellarius `_ * Damian Heard `@DamianHeard `_ * Daniel Imhoff `@dwieeb `_ * `@immerrr `_ * Brett Higgins `@brettdh `_ * Vlad Frolov `@frol `_ * Tuukka Mustonen `@tuukkamustonen `_ * Francois-Xavier Darveau `@EFF `_ * Jérôme Lafréchoux `@lafrech `_ * `@DmitriyS `_ * Svetlozar Argirov `@zaro `_ * Florian S. `@nebularazer `_ * `@daniel98321 `_ * `@Itayazolay `_ * `@Reskov `_ * `@cedzz `_ * F. Moukayed (כוכב) `@kochab `_ * Xiaoyu Lee `@lee3164 `_ * Jonathan Angelo `@jangelo `_ * `@zhenhua32 `_ * Martin Roy `@lindycoder `_ * Kubilay Kocak `@koobs `_ * `@dodumosu `_ * Nate Dellinger `@Nateyo `_ * Karthikeyan Singaravelan `@tirkarthi `_ * Sami Salonen `@suola `_ * Tim Gates `@timgates42 `_ * Lefteris Karapetsas `@lefterisjp `_ * Utku Gultopu `@ugultopu `_ * Jason Williams `@jaswilli `_ * Grey Li `@greyli `_ * `@michaelizergit `_ * Legolas Bloom `@TTWShell `_ * Kevin Kirsche `@kkirsche `_ * Isira Seneviratne `@Isira-Seneviratne `_ * Anton Ostapenko `@AVOstap `_ webargs-8.5.0/CHANGELOG.rst000066400000000000000000001242071461250545000152070ustar00rootroot00000000000000Changelog --------- 8.5.0 (2024-04-25) ****************** Other changes: * Test against Python 3.12. * Async location loading now supports loader functions which are not themselves async, but which return an awaitable result. This means that users who are already handling awaitable objects can return them from non-async loaders and expect ``webargs`` to ``await`` them. This allows some async interactions with supported frameworks, where the ``webargs``-provided parser returns a framework object and the framework can be set to return awaitables, e.g., the Falcon parser. Thanks :user:`j0k2r` for the PR! (:pr:`935`) 8.4.0 (2024-01-07) ****************** Features: * Add a new class attribute, ``empty_value`` to ``DelimitedList`` and ``DelimitedTuple``, with a default of ``""``. This controls the value deserialized when an empty string is seen. ``empty_value`` can be used to handle types other than strings more gracefully, e.g. .. code-block:: python from webargs import fields class IntList(fields.DelimitedList): empty_value = 0 myfield = IntList(fields.Int()) .. note:: ``empty_value`` will be changing in webargs v9.0 to be ``missing`` by default. This will allow use of fields with ``load_default`` to specify handling of the empty value. * The rule for default argument names has been made configurable by overriding the ``get_default_arg_name`` method. This is described in the argument passing documentation. Other changes: * Drop support for Python 3.7, which is EOL. * Type annotations for ``FlaskParser`` have been improved. 8.3.0 (2023-07-10) ****************** Features: * ``webargs.Parser`` now inherits from ``typing.Generic`` and is parametrizable over the type of the request object. Various framework-specific parsers are parametrized over their relevant request object classes. * ``webargs.Parser`` and its subclasses now support passing arguments as a single keyword argument without expanding the parsed data into its components. For more details, see advanced docs on ``Argument Passing and arg_name``. Other changes: * Type annotations have been improved to allow ``Mapping`` for dict-like schemas where previously ``dict`` was used. This makes the type covariant rather than invariant (:issue:`836`). * Test against Python 3.11 (:pr:`787`). 8.2.0 (2022-07-11) ****************** Features: * A new method, ``webargs.Parser.async_parse``, can be used for async-aware parsing from the base parser class. This can handle async location loader functions and async error handlers. * ``webargs.Parser.use_args`` and ``use_kwargs`` can now be used to decorate async functions, and will use ``async_parse`` if the decorated function is also async. They will call the non-async ``parse`` method when used to decorate non-async functions. * As a result of the changes to ``webargs.Parser``, ``FlaskParser``, ``DjangoParser``, and ``FalconParser`` now all support async views. Thanks :user:`Isira-Seneviratne` for the initial PR. Changes: * The implementation of ``AsyncParser`` has changed. Now that ``webargs.Parser`` has built-in support for async usage, the primary purpose of ``AsyncParser`` is to redefine ``parse`` as an alias for ``async_parse`` * Set ``python_requires>=3.7.2`` in package metadata (:pr:`692`). Thanks :user:`kasium` for the PR. 8.1.0 (2022-01-12) ****************** Bug fixes: * Fix publishing type hints per `PEP-561 `_. (:pr:`650`). * Add DelimitedTuple to fields.__all__ (:pr:`678`). * Narrow type of ``argmap`` from ``Mapping`` to ``Dict`` (:pr:`682`). Other changes: * Test against Python 3.10 (:pr:`647`). * Drop support for Python 3.6 (:pr:`673`). * Address distutils deprecation warning in Python 3.10 (:pr:`652`). Thanks :user:`kkirsche` for the PR. * Use postponed evaluation of annotations (:pr:`663`). Thanks :user:`Isira-Seneviratne` for the PR. * Pin mypy version in tox (:pr:`674`). * Improve type annotations for ``__version_info__`` (:pr:`680`). 8.0.1 (2021-08-12) ****************** Bug fixes: * Fix "``DelimitedList`` deserializes empty string as ``['']``" (:issue:`623`). Thanks :user:`TTWSchell` for reporting and for the PR. Other changes: * New documentation theme with `furo`. Thanks to :user:`pradyunsg` for writing furo! * Webargs has a new logo. Thanks to :user:`michaelizergit`! (:issue:`312`) * Don't build universal wheels. We don't support Python 2 anymore. (:pr:`632`) * Make the build reproducible (:pr:`631`). 8.0.0 (2021-04-08) ****************** Features: * Add `Parser.pre_load` as a method for allowing users to modify data before schema loading, but without redefining location loaders. See advanced docs on `Parser pre_load` for usage information. (:pr:`583`) * *Backwards-incompatible*: ``unknown`` defaults to `None` for body locations (`json`, `form` and `json_or_form`) (:issue:`580`). * Detection of fields as "multi-value" for unpacking lists from multi-dict types is now extensible with the ``is_multiple`` attribute. If a field sets ``is_multiple = True`` it will be detected as a multi-value field. If ``is_multiple`` is not set or is set to ``None``, webargs will check if the field is an instance of ``List`` or ``Tuple``. (:issue:`563`) * A new attribute on ``Parser`` objects, ``Parser.KNOWN_MULTI_FIELDS`` can be used to set fields which should be detected as ``is_multiple=True`` even when the attribute is not set (:pr:`592`). See docs on "Multi-Field Detection" for more details. Bug fixes: * ``Tuple`` field now behaves as a "multiple" field (:pr:`585`). 7.0.1 (2020-12-14) ****************** Bug fixes: * Fix `DelimitedList` and `DelimitedTuple` to pass additional keyword arguments through their `_serialize` methods to the child fields and fix type checking on these classes. (:issue:`569`) Thanks to :user:`decaz` for reporting. 7.0.0 (2020-12-10) ****************** Changes: * *Backwards-incompatible*: Drop support for webapp2 (:pr:`565`). * Add type annotations to `Parser` class, `DelimitedList`, and `DelimitedTuple`. (:issue:`566`) 7.0.0b2 (2020-12-01) ******************** Features: * `DjangoParser` now supports the `headers` location. (:issue:`540`) * `FalconParser` now supports a new `media` location, which uses Falcon's `media` decoding. (:issue:`253`) `media` behaves very similarly to the `json` location but also supports any registered media handler. See the `Falcon documentation on media types `_ for more details. Changes: * `FalconParser` defaults to the `media` location instead of `json`. (:issue:`253`) * Test against Python 3.9 (:pr:`552`). * *Backwards-incompatible*: Drop support for Python 3.5 (:pr:`553`). 7.0.0b1 (2020-09-11) ******************** Refactoring: * *Backwards-incompatible*: Remove support for marshmallow2 (:issue:`539`) * *Backwards-incompatible*: Remove `dict2schema` Users desiring the `dict2schema` functionality may now rely upon `marshmallow.Schema.from_dict`. Rewrite any code using `dict2schema` like so: .. code-block:: python import marshmallow as ma # webargs 6.x and older from webargs import dict2schema myschema = dict2schema({"q1", ma.fields.Int()}) # webargs 7.x myschema = ma.Schema.from_dict({"q1", ma.fields.Int()}) Features: * Add ``unknown`` as a parameter to ``Parser.parse``, ``Parser.use_args``, ``Parser.use_kwargs``, and parser instantiation. When set, it will be passed to ``Schema.load``. When not set, the value passed will depend on the parser's settings. If set to ``None``, the schema's default behavior will be used (i.e. no value is passed to ``Schema.load``) and parser settings will be ignored. This allows usages like .. code-block:: python import marshmallow as ma @parser.use_kwargs( {"q1": ma.fields.Int(), "q2": ma.fields.Int()}, location="query", unknown=ma.EXCLUDE ) def foo(q1, q2): ... * Defaults for ``unknown`` may be customized on parser classes via ``Parser.DEFAULT_UNKNOWN_BY_LOCATION``, which maps location names to values to use. Usages are varied, but include .. code-block:: python import marshmallow as ma from webargs.flaskparser import FlaskParser # as well as... class MyParser(FlaskParser): DEFAULT_UNKNOWN_BY_LOCATION = {"query": ma.INCLUDE} parser = MyParser() Setting the ``unknown`` value for a Parser instance has higher precedence. So .. code-block:: python parser = MyParser(unknown=ma.RAISE) will always pass ``RAISE``, even when the location is ``query``. * By default, webargs will pass ``unknown=EXCLUDE`` for all locations except for request bodies (``json``, ``form``, and ``json_or_form``) and path parameters. Request bodies and path parameters will pass ``unknown=RAISE``. This behavior is defined by the default value for ``DEFAULT_UNKNOWN_BY_LOCATION``. Changes: * Registered `error_handler` callbacks are required to raise an exception. If a handler is invoked and no exception is raised, `webargs` will raise a `ValueError` (:issue:`527`) 6.1.1 (2020-09-08) ****************** Bug fixes: * Failure to validate flask headers would produce error data which contained tuples as keys, and was therefore not JSON-serializable. (:issue:`500`) These errors will now extract the headername as the key correctly. Thanks to :user:`shughes-uk` for reporting. 6.1.0 (2020-04-05) ****************** Features: * Add ``fields.DelimitedTuple`` when using marshmallow 3. This behaves as a combination of ``fields.DelimitedList`` and ``marshmallow.fields.Tuple``. It takes an iterable of fields, plus a delimiter (defaults to ``,``), and parses delimiter-separated strings into tuples. (:pr:`509`) * Add ``__str__`` and ``__repr__`` to MultiDictProxy to make it easier to work with (:pr:`488`) Support: * Various docs updates (:pr:`482`, :pr:`486`, :pr:`489`, :pr:`498`, :pr:`508`). Thanks :user:`lefterisjp`, :user:`timgates42`, and :user:`ugultopu` for the PRs. 6.0.0 (2020-02-27) ****************** Features: * ``FalconParser``: Pass request content length to ``req.stream.read`` to provide compatibility with ``falcon.testing`` (:pr:`477`). Thanks :user:`suola` for the PR. * *Backwards-incompatible*: Factorize the ``use_args`` / ``use_kwargs`` branch in all parsers. When ``as_kwargs`` is ``False``, arguments are now consistently appended to the arguments list by the ``use_args`` decorator. Before this change, the ``PyramidParser`` would prepend the argument list on each call to ``use_args``. Pyramid view functions must reverse the order of their arguments. (:pr:`478`) 6.0.0b8 (2020-02-16) ******************** Refactoring: * *Backwards-incompatible*: Use keyword-only arguments (:pr:`472`). 6.0.0b7 (2020-02-14) ******************** Features: * *Backwards-incompatible*: webargs will rewrite the error messages in ValidationErrors to be namespaced under the location which raised the error. The `messages` field on errors will therefore be one layer deeper with a single top-level key. 6.0.0b6 (2020-01-31) ******************** Refactoring: * Remove the cache attached to webargs parsers. Due to changes between webargs v5 and v6, the cache is no longer considered useful. Other changes: * Import ``Mapping`` from ``collections.abc`` in pyramidparser.py (:pr:`471`). Thanks :user:`tirkarthi` for the PR. 6.0.0b5 (2020-01-30) ******************** Refactoring: * *Backwards-incompatible*: `DelimitedList` now requires that its input be a string and always serializes as a string. It can still serialize and deserialize using another field, e.g. `DelimitedList(Int())` is still valid and requires that the values in the list parse as ints. 6.0.0b4 (2020-01-28) ******************** Bug fixes: * :cve:`CVE-2020-7965`: Don't attempt to parse JSON if request's content type is mismatched (bugfix from 5.5.3). 6.0.0b3 (2020-01-21) ******************** Features: * *Backwards-incompatible*: Support Falcon 2.0. Drop support for Falcon 1.x (:pr:`459`). Thanks :user:`dodumosu` and :user:`Nateyo` for the PR. 6.0.0b2 (2020-01-07) ******************** Other changes: * *Backwards-incompatible*: Drop support for Python 2 (:issue:`440`). Thanks :user:`hugovk` for the PR. 6.0.0b1 (2020-01-06) ******************** Features: * *Backwards-incompatible*: Schemas will now load all data from a location, not only data specified by fields. As a result, schemas with validators which examine the full input data may change in behavior. The `unknown` parameter on schemas may be used to alter this. For example, `unknown=marshmallow.EXCLUDE` will produce a behavior similar to webargs v5. Bug fixes: * *Backwards-incompatible*: All parsers now require the Content-Type to be set correctly when processing JSON request bodies. This impacts ``DjangoParser``, ``FalconParser``, ``FlaskParser``, and ``PyramidParser`` Refactoring: * *Backwards-incompatible*: Schema fields may not specify a location any longer, and `Parser.use_args` and `Parser.use_kwargs` now accept `location` (singular) instead of `locations` (plural). Instead of using a single field or schema with multiple `locations`, users are recommended to make multiple calls to `use_args` or `use_kwargs` with a distinct schema per location. For example, code should be rewritten like this: .. code-block:: python # webargs 5.x and older @parser.use_args( { "q1": ma.fields.Int(location="query"), "q2": ma.fields.Int(location="query"), "h1": ma.fields.Int(location="headers"), }, locations=("query", "headers"), ) def foo(q1, q2, h1): ... # webargs 6.x @parser.use_args({"q1": ma.fields.Int(), "q2": ma.fields.Int()}, location="query") @parser.use_args({"h1": ma.fields.Int()}, location="headers") def foo(q1, q2, h1): ... * The `location_handler` decorator has been removed and replaced with `location_loader`. `location_loader` serves the same purpose (letting you write custom hooks for loading data) but its expected method signature is different. See the docs on `location_loader` for proper usage. Thanks :user:`sirosen` for the PR! 5.5.3 (2020-01-28) ****************** Bug fixes: * :cve:`CVE-2020-7965`: Don't attempt to parse JSON if request's content type is mismatched. 5.5.2 (2019-10-06) ****************** Bug fixes: * Handle ``UnicodeDecodeError`` when parsing JSON payloads (:issue:`427`). Thanks :user:`lindycoder` for the catch and patch. 5.5.1 (2019-09-15) ****************** Bug fixes: * Remove usage of deprecated ``Field.fail`` when using marshmallow 3. 5.5.0 (2019-09-07) ****************** Support: * Various docs updates (:pr:`414`, :pr:`421`). Refactoring: * Don't mutate ``globals()`` in ``webargs.fields`` (:pr:`411`). * Use marshmallow 3's ``Schema.from_dict`` if available (:pr:`415`). 5.4.0 (2019-07-23) ****************** Changes: * Use explicit type check for `fields.DelimitedList` when deciding to parse value with `getlist()` (`#406 (comment) `_ ). Support: * Add "Parsing Lists in Query Strings" section to docs (:issue:`406`). 5.3.2 (2019-06-19) ****************** Bug fixes: * marshmallow 3.0.0rc7 compatibility (:pr:`395`). 5.3.1 (2019-05-05) ****************** Bug fixes: * marshmallow 3.0.0rc6 compatibility (:pr:`384`). 5.3.0 (2019-04-08) ****************** Features: * Add `"path"` location to ``AIOHTTPParser``, ``FlaskParser``, and ``PyramidParser`` (:pr:`379`). Thanks :user:`zhenhua32` for the PR. * Add ``webargs.__version_info__``. 5.2.0 (2019-03-16) ****************** Features: * Make the schema class used when generating a schema from a dict overridable (:issue:`375`). Thanks :user:`ThiefMaster`. 5.1.3 (2019-03-11) ****************** Bug fixes: * :cve:`CVE-2019-9710`: Fix race condition between parallel requests when the cache is used (:issue:`371`). Thanks :user:`ThiefMaster` for reporting and fixing. 5.1.2 (2019-02-03) ****************** Bug fixes: * Remove lingering usages of ``ValidationError.status_code`` (:issue:`365`). Thanks :user:`decaz` for reporting. * Avoid ``AttributeError`` on Python<3.5.4 (:issue:`366`). * Fix incorrect type annotations for ``error_headers``. * Fix outdated docs (:issue:`367`). Thanks :user:`alexandersoto` for reporting. 5.1.1.post0 (2019-01-30) ************************ * Include LICENSE in sdist (:issue:`364`). 5.1.1 (2019-01-28) ****************** Bug fixes: * Fix installing ``simplejson`` on Python 2 by distributing a Python 2-only wheel (:issue:`363`). 5.1.0 (2019-01-11) ****************** Features: * Error handlers for `AsyncParser` classes may be coroutine functions. * Add type annotations to `AsyncParser` and `AIOHTTPParser`. Bug fixes: * Fix compatibility with Flask<1.0 (:issue:`355`). Thanks :user:`hoatle` for reporting. * Address warning on Python 3.7 about importing from ``collections.abc``. 5.0.0 (2019-01-03) ****************** Features: * *Backwards-incompatible*: A 400 HTTPError is raised when an invalid JSON payload is passed. (:issue:`329`). Thanks :user:`zedrdave` for reporting. Other changes: * *Backwards-incompatible*: `webargs.argmap2schema` is removed. Use `webargs.dict2schema` instead. * *Backwards-incompatible*: `webargs.ValidationError` is removed. Use `marshmallow.ValidationError` instead. .. code-block:: python # <5.0.0 from webargs import ValidationError def auth_validator(value): # ... raise ValidationError("Authentication failed", status_code=401) @use_args({"auth": fields.Field(validate=auth_validator)}) def auth_view(args): return jsonify(args) # >=5.0.0 from marshmallow import ValidationError def auth_validator(value): # ... raise ValidationError("Authentication failed") @use_args({"auth": fields.Field(validate=auth_validator)}, error_status_code=401) def auth_view(args): return jsonify(args) * *Backwards-incompatible*: Missing arguments will no longer be filled in when using ``@use_kwargs`` (:issue:`342,307,252`). Use ``**kwargs`` to account for non-required fields. .. code-block:: python # <5.0.0 @use_kwargs( {"first_name": fields.Str(required=True), "last_name": fields.Str(required=False)} ) def myview(first_name, last_name): # last_name is webargs.missing if it's missing from the request return {"first_name": first_name} # >=5.0.0 @use_kwargs( {"first_name": fields.Str(required=True), "last_name": fields.Str(required=False)} ) def myview(first_name, **kwargs): # last_name will not be in kwargs if it's missing from the request return {"first_name": first_name} * `simplejson `_ is now a required dependency on Python 2 (:pr:`334`). This ensures consistency of behavior across Python 2 and 3. 4.4.1 (2018-01-03) ****************** Bug fixes: * Remove usages of ``argmap2schema`` from ``fields.Nested``, ``AsyncParser``, and ``PyramidParser``. 4.4.0 (2019-01-03) ****************** * *Deprecation*: ``argmap2schema`` is deprecated in favor of ``dict2schema`` (:pr:`352`). 4.3.1 (2018-12-31) ****************** * Add ``force_all`` param to ``PyramidParser.use_args``. * Add warning about missing arguments to ``AsyncParser``. 4.3.0 (2018-12-30) ****************** * *Deprecation*: Add warning about missing arguments getting added to parsed arguments dictionary (:issue:`342`). This behavior will be removed in version 5.0.0. 4.2.0 (2018-12-27) ****************** Features: * Add ``force_all`` argument to ``use_args`` and ``use_kwargs`` (:issue:`252`, :issue:`307`). Thanks :user:`piroux` for reporting. * *Deprecation*: The ``status_code`` and ``headers`` arguments to ``ValidationError`` are deprecated. Pass ``error_status_code`` and ``error_headers`` to `Parser.parse`, `Parser.use_args`, and `Parser.use_kwargs` instead. (:issue:`327`, :issue:`336`). * Custom error handlers receive ``error_status_code`` and ``error_headers`` arguments. (:issue:`327`). .. code-block:: python # <4.2.0 @parser.error_handler def handle_error(error, req, schema): raise CustomError(error.messages) class MyParser(FlaskParser): def handle_error(self, error, req, schema): # ... raise CustomError(error.messages) # >=4.2.0 @parser.error_handler def handle_error(error, req, schema, status_code, headers): raise CustomError(error.messages) # OR @parser.error_handler def handle_error(error, **kwargs): raise CustomError(error.messages) class MyParser(FlaskParser): def handle_error(self, error, req, schema, status_code, headers): # ... raise CustomError(error.messages) # OR def handle_error(self, error, req, **kwargs): # ... raise CustomError(error.messages) Legacy error handlers will be supported until version 5.0.0. 4.1.3 (2018-12-02) ****************** Bug fixes: * Fix bug in ``AIOHTTParser`` that prevented calling ``use_args`` on the same view function multiple times (:issue:`273`). Thanks to :user:`dnp1` for reporting and :user:`jangelo` for the fix. * Fix compatibility with marshmallow 3.0.0rc1 (:pr:`330`). 4.1.2 (2018-11-03) ****************** Bug fixes: * Fix serialization behavior of ``DelimitedList`` (:pr:`319`). Thanks :user:`lee3164` for the PR. Other changes: * Test against Python 3.7. 4.1.1 (2018-10-25) ****************** Bug fixes: * Fix bug in ``AIOHTTPParser`` that caused a ``JSONDecode`` error when parsing empty payloads (:issue:`229`). Thanks :user:`explosic4` for reporting and thanks user :user:`kochab` for the PR. 4.1.0 (2018-09-17) ****************** Features: * Add ``webargs.testing`` module, which exposes ``CommonTestCase`` to third-party parser libraries (see comments in :pr:`287`). 4.0.0 (2018-07-15) ****************** Features: * *Backwards-incompatible*: Custom error handlers receive the `marshmallow.Schema` instance as the third argument. Update any functions decorated with `Parser.error_handler` to take a ``schema`` argument, like so: .. code-block:: python # 3.x @parser.error_handler def handle_error(error, req): raise CustomError(error.messages) # 4.x @parser.error_handler def handle_error(error, req, schema): raise CustomError(error.messages) See `marshmallow-code/marshmallow#840 (comment) `_ for more information about this change. Bug fixes: * *Backwards-incompatible*: Rename ``webargs.async`` to ``webargs.asyncparser`` to fix compatibility with Python 3.7 (:issue:`240`). Thanks :user:`Reskov` for the catch and patch. Other changes: * *Backwards-incompatible*: Drop support for Python 3.4 (:pr:`243`). Python 2.7 and >=3.5 are supported. * *Backwards-incompatible*: Drop support for marshmallow<2.15.0. marshmallow>=2.15.0 and >=3.0.0b12 are officially supported. * Use `black `_ with `pre-commit `_ for code formatting (:pr:`244`). 3.0.2 (2018-07-05) ****************** Bug fixes: * Fix compatibility with marshmallow 3.0.0b12 (:pr:`242`). Thanks :user:`lafrech`. 3.0.1 (2018-06-06) ****************** Bug fixes: * Respect `Parser.DEFAULT_VALIDATION_STATUS` when a `status_code` is not explicitly passed to `ValidationError` (:issue:`180`). Thanks :user:`foresmac` for finding this. Support: * Add "Returning HTTP 400 Responses" section to docs (:issue:`180`). 3.0.0 (2018-05-06) ****************** Changes: * *Backwards-incompatible*: Custom error handlers receive the request object as the second argument. Update any functions decorated with ``Parser.error_handler`` to take a `req` argument, like so: .. code-block:: python # 2.x @parser.error_handler def handle_error(error): raise CustomError(error.messages) # 3.x @parser.error_handler def handle_error(error, req): raise CustomError(error.messages) * *Backwards-incompatible*: Remove unused ``instance`` and ``kwargs`` arguments of ``argmap2schema``. * *Backwards-incompatible*: Remove ``Parser.load`` method (``Parser`` now calls ``Schema.load`` directly). These changes shouldn't affect most users. However, they might break custom parsers calling these methods. (:pr:`222`) * Drop support for aiohttp<3.0.0. 2.1.0 (2018-04-01) ****************** Features: * Respect ``data_key`` field argument (in marshmallow 3). Thanks :user:`lafrech`. 2.0.0 (2018-02-08) ****************** Changes: * Drop support for aiohttp<2.0.0. * Remove use of deprecated `Request.has_body` attribute in aiohttpparser (:issue:`186`). Thanks :user:`ariddell` for reporting. 1.10.0 (2018-02-08) ******************* Features: * Add support for marshmallow>=3.0.0b7 (:pr:`188`). Thanks :user:`lafrech`. Deprecations: * Support for aiohttp<2.0.0 is deprecated and will be removed in webargs 2.0.0. 1.9.0 (2018-02-03) ****************** Changes: * ``HTTPExceptions`` raised with `webargs.flaskparser.abort` will always have the ``data`` attribute, even if no additional keywords arguments are passed (:pr:`184`). Thanks :user:`lafrech`. Support: * Fix examples in examples/ directory. 1.8.1 (2017-07-17) ****************** Bug fixes: * Fix behavior of ``AIOHTTPParser.use_args`` when ``as_kwargs=True`` is passed with a ``Schema`` (:issue:`179`). Thanks :user:`Itayazolay`. 1.8.0 (2017-07-16) ****************** Features: * ``AIOHTTPParser`` supports class-based views, i.e. ``aiohttp.web.View`` (:issue:`177`). Thanks :user:`daniel98321`. 1.7.0 (2017-06-03) ****************** Features: * ``AIOHTTPParser.use_args`` and ``AIOHTTPParser.use_kwargs`` work with `async def` coroutines (:issue:`170`). Thanks :user:`zaro`. 1.6.3 (2017-05-18) ****************** Support: * Fix Flask error handling docs in "Framework support" section (:issue:`168`). Thanks :user:`nebularazer`. 1.6.2 (2017-05-16) ****************** Bug fixes: * Fix parsing multiple arguments in ``AIOHTTParser`` (:issue:`165`). Thanks :user:`ariddell` for reporting and thanks :user:`zaro` for reporting. 1.6.1 (2017-04-30) ****************** Bug fixes: * Fix form parsing in aiohttp>=2.0.0. Thanks :user:`DmitriyS` for the PR. 1.6.0 (2017-03-14) ****************** Bug fixes: * Fix compatibility with marshmallow 3.x. Other changes: * Drop support for Python 2.6 and 3.3. * Support marshmallow>=2.7.0. 1.5.3 (2017-02-04) ****************** Bug fixes: * Port fix from release 1.5.2 to `AsyncParser`. This fixes :issue:`146` for ``AIOHTTPParser``. * Handle invalid types passed to ``DelimitedList`` (:issue:`149`). Thanks :user:`psconnect-dev` for reporting. 1.5.2 (2017-01-08) ****************** Bug fixes: * Don't add ``marshmallow.missing`` to ``original_data`` when using ``marshmallow.validates_schema(pass_original=True)`` (:issue:`146`). Thanks :user:`lafrech` for reporting and for the fix. Other changes: * Test against Python 3.6. 1.5.1 (2016-11-27) ****************** Bug fixes: * Fix handling missing nested args when ``many=True`` (:issue:`120`, :issue:`145`). Thanks :user:`chavz` and :user:`Bangertm` for reporting. * Fix behavior of ``load_from`` in ``AIOHTTPParser``. 1.5.0 (2016-11-22) ****************** Features: * The ``use_args`` and ``use_kwargs`` decorators add a reference to the undecorated function via the ``__wrapped__`` attribute. This is useful for unit-testing purposes (:issue:`144`). Thanks :user:`EFF` for the PR. Bug fixes: * If ``load_from`` is specified on a field, first check the field name before checking ``load_from`` (:issue:`118`). Thanks :user:`jasonab` for reporting. 1.4.0 (2016-09-29) ****************** Bug fixes: * Prevent error when rendering validation errors to JSON in Flask (e.g. when using Flask-RESTful) (:issue:`122`). Thanks :user:`frol` for the catch and patch. NOTE: Though this is a bugfix, this is a potentially breaking change for code that needs to access the original ``ValidationError`` object. .. code-block:: python # Before @app.errorhandler(422) def handle_validation_error(err): return jsonify({"errors": err.messages}), 422 # After @app.errorhandler(422) def handle_validation_error(err): # The marshmallow.ValidationError is available on err.exc return jsonify({"errors": err.exc.messages}), 422 1.3.4 (2016-06-11) ****************** Bug fixes: * Fix bug in parsing form in Falcon>=1.0. 1.3.3 (2016-05-29) ****************** Bug fixes: * Fix behavior for nullable List fields (:issue:`107`). Thanks :user:`shaicantor` for reporting. 1.3.2 (2016-04-14) ****************** Bug fixes: * Fix passing a schema factory to ``use_kwargs`` (:issue:`103`). Thanks :user:`ksesong` for reporting. 1.3.1 (2016-04-13) ****************** Bug fixes: * Fix memory leak when calling ``parser.parse`` with a ``dict`` in a view (:issue:`101`). Thanks :user:`frankslaughter` for reporting. * aiohttpparser: Fix bug in handling bulk-type arguments. Support: * Massive refactor of tests (:issue:`98`). * Docs: Fix incorrect use_args example in Tornado section (:issue:`100`). Thanks :user:`frankslaughter` for reporting. * Docs: Add "Mixing Locations" section (:issue:`90`). Thanks :user:`tuukkamustonen`. 1.3.0 (2016-04-05) ****************** Features: * Add bulk-type arguments support for JSON parsing by passing ``many=True`` to a ``Schema`` (:issue:`81`). Thanks :user:`frol`. Bug fixes: * Fix JSON parsing in Flask<=0.9.0. Thanks :user:`brettdh` for the PR. * Fix behavior of ``status_code`` argument to ``ValidationError`` (:issue:`85`). This requires **marshmallow>=2.7.0**. Thanks :user:`ParthGandhi` for reporting. Support: * Docs: Add "Custom Fields" section with example of using a ``Function`` field (:issue:`94`). Thanks :user:`brettdh` for the suggestion. 1.2.0 (2016-01-04) ****************** Features: * Add ``view_args`` request location to ``FlaskParser`` (:issue:`82`). Thanks :user:`oreza` for the suggestion. Bug fixes: * Use the value of ``load_from`` as the key for error messages when it is provided (:issue:`83`). Thanks :user:`immerrr` for the catch and patch. 1.1.1 (2015-11-14) ****************** Bug fixes: * aiohttpparser: Fix bug that raised a ``JSONDecodeError`` raised when parsing non-JSON requests using default ``locations`` (:issue:`80`). Thanks :user:`leonidumanskiy` for reporting. * Fix parsing JSON requests that have a vendor media type, e.g. ``application/vnd.api+json``. 1.1.0 (2015-11-08) ****************** Features: * ``Parser.parse``, ``Parser.use_args`` and ``Parser.use_kwargs`` can take a Schema factory as the first argument (:issue:`73`). Thanks :user:`DamianHeard` for the suggestion and the PR. Support: * Docs: Add "Custom Parsers" section with example of parsing nested querystring arguments (:issue:`74`). Thanks :user:`dwieeb`. * Docs: Add "Advanced Usage" page. 1.0.0 (2015-10-19) ****************** Features: * Add ``AIOHTTPParser`` (:issue:`71`). * Add ``webargs.async`` module with ``AsyncParser``. Bug fixes: * If an empty list is passed to a List argument, it will be parsed as an empty list rather than being excluded from the parsed arguments dict (:issue:`70`). Thanks :user:`mTatcher` for catching this. Other changes: * *Backwards-incompatible*: When decorating resource methods with ``FalconParser.use_args``, the parsed arguments dictionary will be positioned **after** the request and response arguments. * *Backwards-incompatible*: When decorating views with ``DjangoParser.use_args``, the parsed arguments dictionary will be positioned **after** the request argument. * *Backwards-incompatible*: ``Parser.get_request_from_view_args`` gets passed a view function as its first argument. * *Backwards-incompatible*: Remove logging from default error handlers. 0.18.0 (2015-10-04) ******************* Features: * Add ``FalconParser`` (:issue:`63`). * Add ``fields.DelimitedList`` (:issue:`66`). Thanks :user:`jmcarp`. * ``TornadoParser`` will parse json with ``simplejson`` if it is installed. * ``BottleParser`` caches parsed json per-request for improved performance. No breaking changes. Yay! 0.17.0 (2015-09-29) ******************* Features: * ``TornadoParser`` returns unicode strings rather than bytestrings (:issue:`41`). Thanks :user:`thomasboyt` for the suggestion. * Add ``Parser.get_default_request`` and ``Parser.get_request_from_view_args`` hooks to simplify ``Parser`` implementations. * *Backwards-compatible*: ``webargs.core.get_value`` takes a ``Field`` as its last argument. Note: this is technically a breaking change, but this won't affect most users since ``get_value`` is only used internally by ``Parser`` classes. Support: * Add ``examples/annotations_example.py`` (demonstrates using Python 3 function annotations to define request arguments). * Fix examples. Thanks :user:`hyunchel` for catching an error in the Flask error handling docs. Bug fixes: * Correctly pass ``validate`` and ``force_all`` params to ``PyramidParser.use_args``. 0.16.0 (2015-09-27) ******************* The major change in this release is that webargs now depends on `marshmallow `_ for defining arguments and validation. Your code will need to be updated to use ``Fields`` rather than ``Args``. .. code-block:: python # Old API from webargs import Arg args = { "name": Arg(str, required=True), "password": Arg(str, validate=lambda p: len(p) >= 6), "display_per_page": Arg(int, default=10), "nickname": Arg(multiple=True), "Content-Type": Arg(dest="content_type", location="headers"), "location": Arg({"city": Arg(str), "state": Arg(str)}), "meta": Arg(dict), } # New API from webargs import fields args = { "name": fields.Str(required=True), "password": fields.Str(validate=lambda p: len(p) >= 6), "display_per_page": fields.Int(load_default=10), "nickname": fields.List(fields.Str()), "content_type": fields.Str(load_from="Content-Type"), "location": fields.Nested({"city": fields.Str(), "state": fields.Str()}), "meta": fields.Dict(), } Features: * Error messages for all arguments are "bundled" (:issue:`58`). Changes: * *Backwards-incompatible*: Replace ``Args`` with marshmallow fields (:issue:`61`). * *Backwards-incompatible*: When using ``use_kwargs``, missing arguments will have the special value ``missing`` rather than ``None``. * ``TornadoParser`` raises a custom ``HTTPError`` with a ``messages`` attribute when validation fails. Bug fixes: * Fix required validation of nested arguments (:issue:`39`, :issue:`51`). These are fixed by virtue of using marshmallow's ``Nested`` field. Thanks :user:`ewang` and :user:`chavz` for reporting. Support: * Updated docs. * Add ``examples/schema_example.py``. * Tested against Python 3.5. 0.15.0 (2015-08-22) ******************* Changes: * If a parsed argument is ``None``, the type conversion function is not called :issue:`54`. Thanks :user:`marcellarius`. Bug fixes: * Fix parsing nested ``Args`` when the argument is missing from the input (:issue:`52`). Thanks :user:`stas`. 0.14.0 (2015-06-28) ******************* Features: * Add parsing of ``matchdict`` to ``PyramidParser``. Thanks :user:`hartror`. Bug fixes: * Fix ``PyramidParser's`` ``use_kwargs`` method (:issue:`42`). Thanks :user:`hartror` for the catch and patch. * Correctly use locations passed to Parser's constructor when using ``use_args`` (:issue:`44`). Thanks :user:`jacebrowning` for the catch and patch. * Fix behavior of ``default`` and ``dest`` argument on nested ``Args`` (:issue:`40` and :issue:`46`). Thanks :user:`stas`. Changes: * A 422 response is returned to the client when a ``ValidationError`` is raised by a parser (:issue:`38`). 0.13.0 (2015-04-05) ******************* Features: * Support for webapp2 via the `webargs.webapp2parser` module. Thanks :user:`Trii`. * Store argument name on ``RequiredArgMissingError``. Thanks :user:`stas`. * Allow error messages for required validation to be overriden. Thanks again :user:`stas`. Removals: * Remove ``source`` parameter from ``Arg``. 0.12.0 (2015-03-22) ******************* Features: * Store argument name on ``ValidationError`` (:issue:`32`). Thanks :user:`alexmic` for the suggestion. Thanks :user:`stas` for the patch. * Allow nesting of dict subtypes. 0.11.0 (2015-03-01) ******************* Changes: * Add ``dest`` parameter to ``Arg`` constructor which determines the key to be added to the parsed arguments dictionary (:issue:`32`). * *Backwards-incompatible*: Rename ``targets`` parameter to ``locations`` in ``Parser`` constructor, ``Parser#parse_arg``, ``Parser#parse``, ``Parser#use_args``, and ``Parser#use_kwargs``. * *Backwards-incompatible*: Rename ``Parser#target_handler`` to ``Parser#location_handler``. Deprecation: * The ``source`` parameter is deprecated in favor of the ``dest`` parameter. Bug fixes: * Fix ``validate`` parameter of ``DjangoParser#use_args``. 0.10.0 (2014-12-23) ******************* * When parsing a nested ``Arg``, filter out extra arguments that are not part of the ``Arg's`` nested ``dict`` (:issue:`28`). Thanks Derrick Gilland for the suggestion. * Fix bug in parsing ``Args`` with both type coercion and ``multiple=True`` (:issue:`30`). Thanks Steven Manuatu for reporting. * Raise ``RequiredArgMissingError`` when a required argument is missing on a request. 0.9.1 (2014-12-11) ****************** * Fix behavior of ``multiple=True`` when nesting Args (:issue:`29`). Thanks Derrick Gilland for reporting. 0.9.0 (2014-12-08) ****************** * Pyramid support thanks to @philtay. * User-friendly error messages when ``Arg`` type conversion/validation fails. Thanks Andriy Yurchuk. * Allow ``use`` argument to be a list of functions. * Allow ``Args`` to be nested within each other, e.g. for nested dict validation. Thanks @saritasa for the suggestion. * *Backwards-incompatible*: Parser will only pass ``ValidationErrors`` to its error handler function, rather than catching all generic Exceptions. * *Backwards-incompatible*: Rename ``Parser.TARGET_MAP`` to ``Parser.__target_map__``. * Add a short-lived cache to the ``Parser`` class that can be used to store processed request data for reuse. * Docs: Add example usage with Flask-RESTful. 0.8.1 (2014-10-28) ****************** * Fix bug in ``TornadoParser`` that raised an error when request body is not a string (e.g when it is a ``Future``). Thanks Josh Carp. 0.8.0 (2014-10-26) ****************** * Fix ``Parser.use_kwargs`` behavior when an ``Arg`` is allowed missing. The ``allow_missing`` attribute is ignored when ``use_kwargs`` is called. * ``default`` may be a callable. * Allow ``ValidationError`` to specify a HTTP status code for the error response. * Improved error logging. * Add ``'query'`` as a valid target name. * Allow a list of validators to be passed to an ``Arg`` or ``Parser.parse``. * A more useful ``__repr__`` for ``Arg``. * Add examples and updated docs. 0.7.0 (2014-10-18) ****************** * Add ``source`` parameter to ``Arg`` constructor. Allows renaming of keys in the parsed arguments dictionary. Thanks Josh Carp. * ``FlaskParser's`` ``handle_error`` method attaches the string representation of validation errors on ``err.data['message']``. The raised exception is stored on ``err.data['exc']``. * Additional keyword arguments passed to ``Arg`` are stored as metadata. 0.6.2 (2014-10-05) ****************** * Fix bug in ``TornadoParser's`` ``handle_error`` method. Thanks Josh Carp. * Add ``error`` parameter to ``Parser`` constructor that allows a custom error message to be used if schema-level validation fails. * Fix bug that raised a ``UnicodeEncodeError`` on Python 2 when an Arg's validator function received non-ASCII input. 0.6.1 (2014-09-28) ****************** * Fix regression with parsing an ``Arg`` with both ``default`` and ``target`` set (see issue #11). 0.6.0 (2014-09-23) ****************** * Add ``validate`` parameter to ``Parser.parse`` and ``Parser.use_args``. Allows validation of the full parsed output. * If ``allow_missing`` is ``True`` on an ``Arg`` for which ``None`` is explicitly passed, the value will still be present in the parsed arguments dictionary. * *Backwards-incompatible*: ``Parser's`` ``parse_*`` methods return ``webargs.core.Missing`` if the value cannot be found on the request. NOTE: ``webargs.core.Missing`` will *not* show up in the final output of ``Parser.parse``. * Fix bug with parsing empty request bodies with ``TornadoParser``. 0.5.1 (2014-08-30) ****************** * Fix behavior of ``Arg's`` ``allow_missing`` parameter when ``multiple=True``. * Fix bug in tornadoparser that caused parsing JSON arguments to fail. 0.5.0 (2014-07-27) ****************** * Fix JSON parsing in Flask parser when Content-Type header contains more than just `application/json`. Thanks Samir Uppaluru for reporting. * *Backwards-incompatible*: The ``use`` parameter to ``Arg`` is called before type conversion occurs. Thanks Eric Wang for the suggestion. * Tested on Tornado>=4.0. 0.4.0 (2014-05-04) ****************** * Custom target handlers can be defined using the ``Parser.target_handler`` decorator. * Error handler can be specified using the ``Parser.error_handler`` decorator. * ``Args`` can define their request target by passing in a ``target`` argument. * *Backwards-incompatible*: ``DEFAULT_TARGETS`` is now a class member of ``Parser``. This allows subclasses to override it. 0.3.4 (2014-04-27) ****************** * Fix bug that caused ``use_args`` to fail on class-based views in Flask. * Add ``allow_missing`` parameter to ``Arg``. 0.3.3 (2014-03-20) ****************** * Awesome contributions from the open-source community! * Add ``use_kwargs`` decorator. Thanks @venuatu. * Tornado support thanks to @jvrsantacruz. * Tested on Python 3.4. 0.3.2 (2014-03-04) ****************** * Fix bug with parsing JSON in Flask and Bottle. 0.3.1 (2014-03-03) ****************** * Remove print statements in core.py. Oops. 0.3.0 (2014-03-02) ****************** * Add support for repeated parameters (#1). * *Backwards-incompatible*: All `parse_*` methods take `arg` as their fourth argument. * Add ``error_handler`` param to ``Parser``. 0.2.0 (2014-02-26) ****************** * Bottle support. * Add ``targets`` param to ``Parser``. Allows setting default targets. * Add ``files`` target. 0.1.0 (2014-02-16) ****************** * First release. * Parses JSON, querystring, forms, headers, and cookies. * Support for Flask and Django. webargs-8.5.0/CODE_OF_CONDUCT.md000066400000000000000000000001341461250545000157550ustar00rootroot00000000000000For the code of conduct, see https://marshmallow.readthedocs.io/en/dev/code_of_conduct.html webargs-8.5.0/CONTRIBUTING.rst000066400000000000000000000103231461250545000156200ustar00rootroot00000000000000Contributing Guidelines ======================= Security Contact Information ---------------------------- To report a security vulnerability, please use the `Tidelift security contact `_. Tidelift will coordinate the fix and disclosure. Questions, Feature Requests, Bug Reports, and Feedback… ------------------------------------------------------- …should all be reported on the `GitHub Issue Tracker`_ . .. _`GitHub Issue Tracker`: https://github.com/marshmallow-code/webargs/issues?state=open Contributing Code ----------------- Integration with a Another Web Framework… +++++++++++++++++++++++++++++++++++++++++ …should be released as a separate package. **Pull requests adding support for another framework will not be accepted**. In order to keep webargs small and easy to maintain, we are not currently adding support for more frameworks. Instead, release your framework integration as a separate package and add it to the `Ecosystem `_ page in the `GitHub wiki `_ . Setting Up for Local Development ++++++++++++++++++++++++++++++++ 1. Fork webargs_ on GitHub. :: $ git clone https://github.com/marshmallow-code/webargs.git $ cd webargs 2. Install development requirements. **It is highly recommended that you use a virtualenv.** Use the following command to install an editable version of webargs along with its development requirements. :: # After activating your virtualenv $ pip install -e '.[dev]' 3. (Optional, but recommended) Install the pre-commit hooks, which will format and lint your git staged files. :: # The pre-commit CLI was installed above $ pre-commit install Git Branch Structure ++++++++++++++++++++ Webargs abides by the following branching model: ``dev`` Current development branch. **New features should branch off here**. ``X.Y-line`` 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. **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. Pull Requests ++++++++++++++ 1. Create a new local branch. :: # For a new feature $ git checkout -b name-of-feature dev # For a bugfix $ git checkout -b fix-something 1.2-line 2. Commit your changes. Write `good commit messages `_. :: $ git commit -m "Detailed commit message" $ git push origin name-of-feature 3. Before submitting a pull request, check the following: - If the pull request adds functionality, it is tested and the docs are updated. - You've added yourself to ``AUTHORS.rst``. 4. Submit a pull request to ``marshmallow-code:dev`` or the appropriate maintenance branch. The `CI `_ build must be passing before your pull request is merged. Running Tests +++++++++++++ To run all tests: :: $ pytest To run syntax checks: :: $ tox -e lint (Optional) To run tests in all supported Python versions in their own virtual environments (must have each interpreter installed): :: $ tox Documentation +++++++++++++ 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_. To build the docs in "watch" mode: :: $ tox -e watch-docs Changes in the `docs/` directory will automatically trigger a rebuild. Contributing Examples +++++++++++++++++++++ Have a usage example you'd like to share? Feel free to add it to the `examples `_ directory and send a pull request. .. _Sphinx: http://sphinx.pocoo.org/ .. _`reStructuredText`: https://docutils.sourceforge.io/rst.html .. _webargs: https://github.com/marshmallow-code/webargs webargs-8.5.0/LICENSE000066400000000000000000000020501461250545000141620ustar00rootroot00000000000000Copyright Steven Loria and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. webargs-8.5.0/NOTICE000066400000000000000000000060651461250545000140730ustar00rootroot00000000000000webargs includes some code from third-party libraries. Flask-Restful License ===================== Copyright (c) 2013, Twilio, Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - Neither the name of the Twilio, Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Werkzeug License ================ Copyright (c) 2014 by the Werkzeug Team, see AUTHORS for more details. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * The names of the contributors may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. webargs-8.5.0/README.rst000066400000000000000000000070621461250545000146540ustar00rootroot00000000000000******* webargs ******* .. image:: https://badgen.net/pypi/v/webargs :target: https://pypi.org/project/webargs/ :alt: PyPI version .. image:: https://github.com/marshmallow-code/webargs/actions/workflows/build-release.yml/badge.svg :target: https://github.com/marshmallow-code/webargs/actions/workflows/build-release.yml :alt: Build status .. image:: https://readthedocs.org/projects/webargs/badge/ :target: https://webargs.readthedocs.io/ :alt: Documentation .. image:: https://badgen.net/badge/marshmallow/3 :target: https://marshmallow.readthedocs.io/en/latest/upgrading.html :alt: marshmallow 3 compatible Homepage: https://webargs.readthedocs.io/ 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. .. code-block:: python from flask import Flask from webargs import fields from webargs.flaskparser import use_args app = Flask(__name__) @app.route("/") @use_args({"name": fields.Str(required=True)}, location="query") def index(args): return "Hello " + args["name"] if __name__ == "__main__": app.run() # curl http://localhost:5000/\?name\='World' # Hello World Install ======= :: pip install -U webargs webargs supports Python >= 3.8. Documentation ============= Full documentation is available at https://webargs.readthedocs.io/. Support webargs =============== webargs is maintained by a group of `volunteers `_. If you'd like to support the future of the project, please consider contributing to our Open Collective: .. image:: https://opencollective.com/marshmallow/donate/button.png :target: https://opencollective.com/marshmallow :width: 200 :alt: Donate to our collective Professional Support ==================== Professionally-supported webargs is available through the `Tidelift Subscription `_. Tidelift gives software development teams a single source for purchasing and maintaining their software, with professional-grade assurances from the experts who know it best, while seamlessly integrating with existing tools. [`Get professional support`_] .. _`Get professional support`: https://tidelift.com/subscription/pkg/pypi-webargs?utm_source=pypi-webargs&utm_medium=referral&utm_campaign=readme .. image:: https://user-images.githubusercontent.com/2379650/45126032-50b69880-b13f-11e8-9c2c-abd16c433495.png :target: https://tidelift.com/subscription/pkg/pypi-webargs?utm_source=pypi-webargs&utm_medium=referral&utm_campaign=readme :alt: Get supported marshmallow with Tidelift Security Contact Information ============================ To report a security vulnerability, please use the `Tidelift security contact `_. Tidelift will coordinate the fix and disclosure. Project Links ============= - Docs: https://webargs.readthedocs.io/ - Changelog: https://webargs.readthedocs.io/en/latest/changelog.html - Contributing Guidelines: https://webargs.readthedocs.io/en/latest/contributing.html - PyPI: https://pypi.python.org/pypi/webargs - Issues: https://github.com/marshmallow-code/webargs/issues - Ecosystem / related packages: https://github.com/marshmallow-code/webargs/wiki/Ecosystem License ======= MIT licensed. See the `LICENSE `_ file for more details. webargs-8.5.0/RELEASING.md000066400000000000000000000004651461250545000150200ustar00rootroot00000000000000# Releasing 1. Bump version in `pyproject.toml` and update the changelog with today's date. 2. Commit: `git commit -m "Bump version and update changelog"` 3. Tag the commit: `git tag x.y.z` 4. Push: `git push --tags origin dev`. CI will take care of the PyPI release. 5. Add release notes on Tidelift. webargs-8.5.0/SECURITY.md000066400000000000000000000003001461250545000147420ustar00rootroot00000000000000# Security Contact Information To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. webargs-8.5.0/docs/000077500000000000000000000000001461250545000141105ustar00rootroot00000000000000webargs-8.5.0/docs/Makefile000066400000000000000000000151711461250545000155550ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(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/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/complexity.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/complexity.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/complexity" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/complexity" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."webargs-8.5.0/docs/_static/000077500000000000000000000000001461250545000155365ustar00rootroot00000000000000webargs-8.5.0/docs/_static/logo.png000066400000000000000000000160551461250545000172130ustar00rootroot00000000000000PNG  IHDR>UIDATx^y|TdOK!X" [VubŶԷ- KU ,AEY@H~L,$;sgnΝ?9~ws Ŵp P 01 Σ$@3H(`; M95K=mx]e ţ͛Z!9 ax p}1yӴ#\\`d=<+?xթO?~rX-جVą7W}`k[w@qy~ՆQm;?>*^:,p}<Ա7~}}j}>7 eB 낹?u}bVz Z X1qOQ)򲫾mAݺ#G?1;h +8#^~bk޿aؒ~y((wh&`) bBqkD|tb֡bMalM?Ғ _9ڂ0u"" 6< c;`:NdN&eS=XlbPl6pmj }Wl\d8JeWTJ U,X툏h!:18<D|~]Day)Wv9!?ƈ0cz,e FQYi5E[Va o'8&6rKn^fXG7 AǨ昘xa(}Uw؏ 9(( C&YDW(oxzЊiRԑnJ5:@+VU64i_uh8r9ၯ(`Wgűic@K9 ŧ%R݇vII ;cKZܱWPT^Z{n}e \0]\k&t_6wwہΙe>p:jC0LIi϶|,O¼jKBrӟ\UK1''^q4<@? tGƕkuNPi׏ved9ڴ% (|kSq4W$, \͎ ߾ۭ_@vIQw'g;ťY-l|KlUe1SSħi䕖YNU7rsA:oƅ")˿Req% }?_VǦaQ96u ,=7@߁ 9q"W9C7bؚ~'ru-'SO C\r q]d4lIN#5?/u \34\ 䃶 oqI9pp Eee, }R@wd/V;JO*KgYQb@ƭF)`-2P6rƝqIXW4OmMN9I Z.+ h{MwMTBȓ6+S8v~եq?hl(\!.)&u7BOg{l7;r~X ?\W oG#DS/sAՍ Lbik#| ws9eSSb~=iyms :fGߘ!Sp$ *?:wƍCy9!Yp(l~>H`gu#\uK˃&`ps EJ=2Ґ]\ r]mKՍ C#pSX<nz_lK?39'sؙsAZvX01n73 {̫^ Nl(m Bs#Ϸ& g!5/rKOYL$%a h:` }'52 sWZnXj ȡɾze'p[0ڄE!/N75YM;ئjF Lk$蚭KOQwϳD|oosPZg5c\rKnڑԤ%d9&ڈ%=&(g@mhN{ 1m&>ctv_X뢚ĩuě6#Uou;5׶&8>ZҙKg3WunAS]΃:<&9U$~!A%>}ȔuXMBq׫e+.p\C@ZIa~V _Z$$)`OhUk+`{vN~?&N_1gPV|XNr5_}^\zreM(. ) lmvH>}W Q"ʔy\u-Zm _3_U䞤TmߴPl>{Q\^Jj&K @8,σEvq(Ol *ZF`X뎘C~.\Z7[l̢<%2sңyp4k z):̢|%]5"ѫy&?~QI=S{uɷUTkM[6Ӷ^;pkXw(d#ױp_,`{0DŽv]L !7KLvKO7YQ@e ߝ:5Z1-jXzǯ\rLJV~}f\(`לD6Ӯ+^38 b٩8mg* ص,<$u ZONDfI!ظ1="@^ 6* oXvَ0&0TbjҋʴϗmFXǪXn8ٹCo%RJ.nY:S:k6~-xW>\#>^#gฮ}ct2 M3Vn^l?|Gس^L?E5eF R cI0k^׵c\/+£l;2$?wFc,3 p mxXy'OrWZP;c|ϕ1r=7ݦQZnbwF.#,+'a s +gCӑQ%jge\ DߘXVT>.faǥ4\*]%76Brң-7ax|^5TܽlR8}3)WbN*6$=bbwyô> vkӎ̳WS2nqs ;Vg ((ӷ>'!7bؼc1ȚR`za ȑ_xœ?R!2u_mjފj_}Xz's/{{WI$E5z`ZѦ򕩌+KY$܏L/>N]B͂B1#m|!3_c҆b)r'ѾW~)dXI{Vg&!`1%֝;2>e ѧylL@qG@Zgq(3˴/}7ع.?ɼTZ\:mJ vyP~VdUfuh ȕlG8}յIK7蔭KĒSQu X.b\eU[ӮRӇYTPqzK{/`ڄGabb/߼fj XsKiH/C@꣼{W_r}}Za鈟^i$mtz}I@U:EZ PZI (HV)4b;PSh h%@k%v$  XA$JJH@ANI$ۑ(`BH@+ X+)# P :&VVRlG t M"(`؎$@+DZ PZI (HV)4b;PSh h%@k%v$  XA$JJH@ANI$ۑ(`BH@+ X+)# P :&VVRlG t M"(`؎$@+DZ PZI (HV)4b;PSh h%@k%v$  XA$JJH@ANI$ۑ(`BH@+ X+)# P :&VVRlG t M"(`؎$@+DZ PZI (HV)4b;PSh h%@k%v$  XA$JJH@ANI$ۑ(`BH@+ X+)# P :&V4UpIENDB`webargs-8.5.0/docs/_templates/000077500000000000000000000000001461250545000162455ustar00rootroot00000000000000webargs-8.5.0/docs/_templates/donate.html000066400000000000000000000002761461250545000204120ustar00rootroot00000000000000{% if donate_url %} {% endif %} webargs-8.5.0/docs/_templates/sponsors.html000066400000000000000000000006711461250545000210250ustar00rootroot00000000000000
{% if tidelift_url %} {% endif %}
webargs-8.5.0/docs/advanced.rst000066400000000000000000000626171461250545000164230ustar00rootroot00000000000000Advanced Usage ============== This section includes guides for advanced usage patterns. Custom Location Handlers ------------------------ To add your own custom location handler, write a function that receives a request, and a :class:`Schema `, then decorate that function with :func:`Parser.location_loader `. .. code-block:: python from webargs import fields from webargs.flaskparser import parser @parser.location_loader("data") def load_data(request, schema): return request.data # Now 'data' can be specified as a location @parser.use_args({"per_page": fields.Int()}, location="data") def posts(args): return "displaying {} posts".format(args["per_page"]) .. NOTE:: The schema is passed so that it can be used to wrap multidict types and unpack List fields correctly. If you are writing a loader for a multidict type, consider looking at :class:`MultiDictProxy ` for an example of how to do this. "meta" Locations ~~~~~~~~~~~~~~~~ You can define your own locations which mix data from several existing locations. The `json_or_form` location does this -- first trying to load data as JSON and then falling back to a form body -- and its implementation is quite simple: .. code-block:: python def load_json_or_form(self, req, schema): """Load data from a request, accepting either JSON or form-encoded data. The data will first be loaded as JSON, and, if that fails, it will be loaded as a form post. """ data = self.load_json(req, schema) if data is not missing: return data return self.load_form(req, schema) You can imagine your own locations with custom behaviors like this. For example, to mix query parameters and form body data, you might write the following: .. code-block:: python from webargs import fields from webargs.multidictproxy import MultiDictProxy from webargs.flaskparser import parser @parser.location_loader("query_and_form") def load_data(request, schema): # relies on the Flask (werkzeug) MultiDict type's implementation of # these methods, but when you're extending webargs, you may know things # about your framework of choice newdata = request.args.copy() newdata.update(request.form) return MultiDictProxy(newdata, schema) # Now 'query_and_form' means you can send these values in either location, # and they will be *mixed* together into a new dict to pass to your schema @parser.use_args({"favorite_food": fields.String()}, location="query_and_form") def set_favorite_food(args): ... # do stuff return "your favorite food is now set to {}".format(args["favorite_food"]) marshmallow Integration ----------------------- When you need more flexibility in defining input schemas, you can pass a marshmallow `Schema ` instead of a dictionary to `Parser.parse `, `Parser.use_args `, and `Parser.use_kwargs `. .. code-block:: python from marshmallow import Schema, fields from webargs.flaskparser import use_args class UserSchema(Schema): id = fields.Int(dump_only=True) # read-only (won't be parsed by webargs) username = fields.Str(required=True) password = fields.Str(load_only=True) # write-only first_name = fields.Str(load_default="") last_name = fields.Str(load_default="") date_registered = fields.DateTime(dump_only=True) @use_args(UserSchema()) def profile_view(args): username = args["username"] # ... @use_kwargs(UserSchema()) def profile_update(username, password, first_name, last_name): update_profile(username, password, first_name, last_name) # ... # You can add additional parameters @use_kwargs({"posts_per_page": fields.Int(load_default=10)}, location="query") @use_args(UserSchema()) def profile_posts(args, posts_per_page): username = args["username"] # ... .. _advanced_setting_unknown: Setting `unknown` ----------------- webargs supports several ways of setting and passing the `unknown` parameter for `handling unknown fields `_. You can pass `unknown=...` as a parameter to any of `Parser.parse `, `Parser.use_args `, and `Parser.use_kwargs `. .. note:: The `unknown` value is passed to the schema's `load()` call. It therefore only applies to the top layer when nesting is used. To control `unknown` at multiple layers of a nested schema, you must use other mechanisms, like the `unknown` argument to `fields.Nested`. Default `unknown` ~~~~~~~~~~~~~~~~~ By default, webargs will pass `unknown=marshmallow.EXCLUDE` except when the location is `json`, `form`, `json_or_form`, or `path`. In those cases, it uses `unknown=marshmallow.RAISE` instead. You can change these defaults by overriding `DEFAULT_UNKNOWN_BY_LOCATION`. This is a mapping of locations to values to pass. For example, .. code-block:: python from flask import Flask from marshmallow import EXCLUDE, fields from webargs.flaskparser import FlaskParser app = Flask(__name__) class Parser(FlaskParser): DEFAULT_UNKNOWN_BY_LOCATION = {"query": EXCLUDE} parser = Parser() # location is "query", which is listed in DEFAULT_UNKNOWN_BY_LOCATION, # so EXCLUDE will be used @app.route("/", methods=["GET"]) @parser.use_args({"foo": fields.Int()}, location="query") def get(args): return f"foo x 2 = {args['foo'] * 2}" # location is "json", which is not in DEFAULT_UNKNOWN_BY_LOCATION, # so no value will be passed for `unknown` @app.route("/", methods=["POST"]) @parser.use_args({"foo": fields.Int(), "bar": fields.Int()}, location="json") def post(args): return f"foo x bar = {args['foo'] * args['bar']}" You can also define a default at parser instantiation, which will take precedence over these defaults, as in .. code-block:: python from marshmallow import INCLUDE parser = Parser(unknown=INCLUDE) # because `unknown` is set on the parser, `DEFAULT_UNKNOWN_BY_LOCATION` has # effect and `INCLUDE` will always be used @app.route("/", methods=["POST"]) @parser.use_args({"foo": fields.Int(), "bar": fields.Int()}, location="json") def post(args): unexpected_args = [k for k in args.keys() if k not in ("foo", "bar")] return f"foo x bar = {args['foo'] * args['bar']}; unexpected args={unexpected_args}" Using Schema-Specfied `unknown` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you wish to use the value of `unknown` specified by a schema, simply pass ``unknown=None``. This will disable webargs' automatic passing of values for ``unknown``. For example, .. code-block:: python from flask import Flask from marshmallow import Schema, fields, EXCLUDE, missing from webargs.flaskparser import use_args class RectangleSchema(Schema): length = fields.Float() width = fields.Float() class Meta: unknown = EXCLUDE app = Flask(__name__) # because unknown=None was passed, no value is passed during schema loading # as a result, the schema's behavior (EXCLUDE) is used @app.route("/", methods=["POST"]) @use_args(RectangleSchema(), location="json", unknown=None) def get(args): return f"area = {args['length'] * args['width']}" You can also set ``unknown=None`` when instantiating a parser to make this behavior the default for a parser. When to avoid `use_kwargs` -------------------------- Any `Schema ` passed to `use_kwargs ` MUST deserialize to a dictionary of data. If your schema has a `post_load ` method that returns a non-dictionary, you should use `use_args ` instead. .. code-block:: python from marshmallow import Schema, fields, post_load from webargs.flaskparser import use_args class Rectangle: def __init__(self, length, width): self.length = length self.width = width class RectangleSchema(Schema): length = fields.Float() width = fields.Float() @post_load def make_object(self, data, **kwargs): return Rectangle(**data) @use_args(RectangleSchema) def post(rect: Rectangle): return f"Area: {rect.length * rect.width}" Packages such as `marshmallow-sqlalchemy `_ and `marshmallow-dataclass `_ generate schemas that deserialize to non-dictionary objects. Therefore, `use_args ` should be used with those schemas. Schema Factories ---------------- If you need to parametrize a schema based on a given request, you can use a "Schema factory": a callable that receives the current `request` and returns a `marshmallow.Schema` instance. Consider the following use cases: - Filtering via a query parameter by passing ``only`` to the Schema. - Handle partial updates for PATCH requests using marshmallow's `partial loading `_ API. .. code-block:: python from flask import Flask from marshmallow import Schema, fields from webargs.flaskparser import use_args app = Flask(__name__) class UserSchema(Schema): id = fields.Int(dump_only=True) username = fields.Str(required=True) password = fields.Str(load_only=True) first_name = fields.Str(load_default="") last_name = fields.Str(load_default="") date_registered = fields.DateTime(dump_only=True) def make_user_schema(request): # Filter based on 'fields' query parameter fields = request.args.get("fields", None) only = fields.split(",") if fields else None # Respect partial updates for PATCH requests partial = request.method == "PATCH" # Add current request to the schema's context return UserSchema(only=only, partial=partial, context={"request": request}) # Pass the factory to .parse, .use_args, or .use_kwargs @app.route("/profile/", methods=["GET", "POST", "PATCH"]) @use_args(make_user_schema) def profile_view(args): username = args.get("username") # ... Reducing Boilerplate ~~~~~~~~~~~~~~~~~~~~ We can reduce boilerplate and improve [re]usability with a simple helper function: .. code-block:: python from webargs.flaskparser import use_args def use_args_with(schema_cls, schema_kwargs=None, **kwargs): schema_kwargs = schema_kwargs or {} def factory(request): # Filter based on 'fields' query parameter only = request.args.get("fields", None) # Respect partial updates for PATCH requests partial = request.method == "PATCH" return schema_cls( only=only, partial=partial, context={"request": request}, **schema_kwargs ) return use_args(factory, **kwargs) Now we can attach input schemas to our view functions like so: .. code-block:: python @use_args_with(UserSchema) def profile_view(args): # ... get_profile(**args) Custom Fields ------------- See the "Custom Fields" section of the marshmallow docs for a detailed guide on defining custom fields which you can pass to webargs parsers: https://marshmallow.readthedocs.io/en/latest/custom_fields.html. Using ``Method`` and ``Function`` Fields with webargs ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Using the :class:`Method ` and :class:`Function ` fields requires that you pass the ``deserialize`` parameter. .. code-block:: python @use_args({"cube": fields.Function(deserialize=lambda x: int(x) ** 3)}) def math_view(args): cube = args["cube"] # ... .. _custom-loaders: Custom Parsers -------------- To add your own parser, extend :class:`Parser ` and implement the `load_*` method(s) you need to override. For example, here is a custom Flask parser that handles nested query string arguments. .. code-block:: python import re from webargs.flaskparser import FlaskParser class NestedQueryFlaskParser(FlaskParser): """Parses nested query args This parser handles nested query args. It expects nested levels delimited by a period and then deserializes the query args into a nested dict. For example, the URL query params `?name.first=John&name.last=Boone` will yield the following dict: { 'name': { 'first': 'John', 'last': 'Boone', } } """ def load_querystring(self, req, schema): return _structure_dict(req.args) def _structure_dict(dict_): def structure_dict_pair(r, key, value): m = re.match(r"(\w+)\.(.*)", key) if m: if r.get(m.group(1)) is None: r[m.group(1)] = {} structure_dict_pair(r[m.group(1)], m.group(2), value) else: r[key] = value r = {} for k, v in dict_.items(): structure_dict_pair(r, k, v) return r Parser pre_load --------------- Similar to ``@pre_load`` decorated hooks on marshmallow Schemas, :class:`Parser ` classes define a method, `pre_load ` which can be overridden to provide per-parser transformations of data. The only way to make use of `pre_load ` is to subclass a :class:`Parser ` and provide an implementation. `pre_load ` is given the data fetched from a location, the schema which will be used, the request object, and the location name which was requested. For example, to define a ``FlaskParser`` which strips whitespace from ``form`` and ``query`` data, one could write the following: .. code-block:: python from webargs.flaskparser import FlaskParser import typing def _strip_whitespace(value): if isinstance(value, str): value = value.strip() elif isinstance(value, typing.Mapping): return {k: _strip_whitespace(value[k]) for k in value} elif isinstance(value, (list, tuple)): return type(value)(map(_strip_whitespace, value)) return value class WhitspaceStrippingFlaskParser(FlaskParser): def pre_load(self, location_data, *, schema, req, location): if location in ("query", "form"): return _strip_whitespace(location_data) return location_data Note that `Parser.pre_load ` is run after location loading but before ``Schema.load`` is called. It can therefore be called on multiple types of mapping objects, including :class:`MultiDictProxy `, depending on what the location loader returns. Returning HTTP 400 Responses ---------------------------- If you'd prefer validation errors to return status code ``400`` instead of ``422``, you can override ``DEFAULT_VALIDATION_STATUS`` on a :class:`Parser `. Subclass the parser for your framework to do so. For example, using Falcon: .. code-block:: python from webargs.falconparser import FalconParser class Parser(FalconParser): DEFAULT_VALIDATION_STATUS = 400 parser = Parser() use_args = parser.use_args use_kwargs = parser.use_kwargs Bulk-type Arguments ------------------- In order to parse a JSON array of objects, pass ``many=True`` to your input ``Schema`` . For example, you might implement JSON PATCH according to `RFC 6902 `_ like so: .. code-block:: python from webargs import fields from webargs.flaskparser import use_args from marshmallow import Schema, validate class PatchSchema(Schema): op = fields.Str( required=True, validate=validate.OneOf(["add", "remove", "replace", "move", "copy"]), ) path = fields.Str(required=True) value = fields.Str(required=True) @app.route("/profile/", methods=["patch"]) @use_args(PatchSchema(many=True)) def patch_blog(args): """Implements JSON Patch for the user profile Example JSON body: [ {"op": "replace", "path": "/email", "value": "mynewemail@test.org"} ] """ # ... Multi-Field Detection --------------------- If a ``List`` field is used to parse data from a location like query parameters -- where one or multiple values can be passed for a single parameter name -- then webargs will automatically treat that field as a list and parse multiple values if present. To implement this behavior, webargs will examine schemas for ``marshmallow.fields.List`` fields. ``List`` fields get unpacked to list values when data is loaded, and other fields do not. This also applies to fields which inherit from ``List``. .. note:: In webargs v8, ``Tuple`` will be treated this way as well, in addition to ``List``. What if you have a list which should be treated as a "multi-field" but which does not inherit from ``List``? webargs offers two solutions. You can add the custom attribute `is_multiple=True` to your field or you can add your class to your parser's list of `KNOWN_MULTI_FIELDS`. First, let's define a "multiplexing field" which takes a string or list of strings to serve as an example: .. code-block:: python # a custom field class which can accept values like List(String()) or String() class CustomMultiplexingField(fields.String): def _deserialize(self, value, attr, data, **kwargs): if isinstance(value, str): return super()._deserialize(value, attr, data, **kwargs) return [ self._deserialize(v, attr, data, **kwargs) for v in value if isinstance(v, str) ] def _serialize(self, value, attr, **kwargs): if isinstance(value, str): return super()._serialize(value, attr, **kwargs) return [self._serialize(v, attr, **kwargs) for v in value if isinstance(v, str)] If you control the definition of ``CustomMultiplexingField``, you can just add ``is_multiple=True`` to it: .. code-block:: python # option 1: define the field with is_multiple = True from webargs.flaskparser import parser class CustomMultiplexingField(fields.Field): is_multiple = True # <----- this marks this as a multi-field ... # as above If you don't control the definition of ``CustomMultiplexingField``, for example because it comes from a library, you can add it to the list of known multifields: .. code-block:: python # option 2: add the field to the parer's list of multi-fields class MyParser(FlaskParser): KNOWN_MULTI_FIELDS = list(FlaskParser.KNOWN_MULTI_FIELDS) + [ CustomMultiplexingField ] parser = MyParser() In either case, the end result is that you can use the multifield and it will be detected as a list when unpacking query string data: .. code-block:: python # gracefully handles # ...?foo=a # ...?foo=a&foo=b # and treats them as ["a"] and ["a", "b"] respectively @parser.use_args({"foo": CustomMultiplexingField()}, location="query") def show_foos(foo): ... Mixing Locations ---------------- Arguments for different locations can be specified by passing ``location`` to each `use_args ` call: .. code-block:: python # "json" is the default, used explicitly below @app.route("/stacked", methods=["POST"]) @use_args({"page": fields.Int(), "q": fields.Str()}, location="query") @use_args({"name": fields.Str()}, location="json") def viewfunc(query_parsed, json_parsed): page = query_parsed["page"] name = json_parsed["name"] # ... To reduce boilerplate, you could create shortcuts, like so: .. code-block:: python import functools query = functools.partial(use_args, location="query") body = functools.partial(use_args, location="json") @query({"page": fields.Int(), "q": fields.Int()}) @body({"name": fields.Str()}) def viewfunc(query_parsed, json_parsed): page = query_parsed["page"] name = json_parsed["name"] # ... Argument Passing and ``arg_name`` --------------------------------- .. NOTE:: This section describes behaviors which are planned to change in ``webargs`` version 9. In version 8, behavior will be as follows. In version 9, ``USE_ARGS_POSITIONAL`` will be removed and will always be ``False``. By default, ``webargs`` provides two ways of passing arguments via decorators, `Parser.use_args `, and `Parser.use_kwargs `. ``use_args`` passes parsed arguments as positionals, and ``use_kwargs`` expands dict-like parsed arguments into keyword arguments. For ``use_args``, the result is that sometimes it is non-obvious which order arguments will be passed in. Consider the following nearly identical example snippets: .. code-block:: python # correct ordering, top-to-bottom @use_args({"foo": fields.Int(), "bar": fields.Str()}, location="query") @use_args({"baz": fields.Str()}, location="json") def viewfunc(query_args, json_args): ... # incorrect ordering, bottom-to-top @use_args({"foo": fields.Int(), "bar": fields.Str()}, location="query") @use_args({"baz": fields.Str()}, location="json") def viewfunc(json_args, query_args): ... To resolve this ambiguity, ``webargs`` version 9 will pass arguments from ``use_args`` as keyword arguments. You can opt-in to this behavior today by setting ``USE_ARGS_POSITIONAL = False`` on a parser class. This will cause webargs to pass arguments named ``{location}_args`` for each location used. For example, .. code-block:: python from webargs.flaskparser import FlaskParser from flask import Flask class KeywordOnlyParser(FlaskParser): USE_ARGS_POSITIONAL = False app = Flask(__name__) parser = KeywordOnlyParser() @app.route("/") @parser.use_args({"foo": fields.Int(), "bar": fields.Str()}, location="query") @parser.use_args({"baz": fields.Str()}, location="json") def myview(*, query_args, json_args): ... You can also customize the names of passed arguments using the ``arg_name`` parameter: .. code-block:: python @app.route("/") @parser.use_args( {"foo": fields.Int(), "bar": fields.Str()}, location="query", arg_name="query" ) @parser.use_args({"baz": fields.Str()}, location="json", arg_name="payload") def myview(*, query, payload): ... Note that ``arg_name`` is available even on parsers where ``USE_ARGS_POSITIONAL`` is not set. Using an Alternate Argument Name Convention ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ As described above, the default naming convention for ``use_args`` arguments is ``{location}_args``. You can customize this by creating a parser class and overriding the ``get_default_arg_name`` method. ``get_default_arg_name`` takes the ``location`` and the ``schema`` as arguments. The default implementation is: .. code-block:: python def get_default_arg_name(self, location, schema): return f"{location}_args" You can customize this to set different arg names. For example, .. code-block:: python from webargs.flaskparser import FlaskParser class MyParser(FlaskParser): USE_ARGS_POSITIONAL = False def get_default_arg_name(self, location, schema): if location in ("json", "form", "json_or_form"): return "body" elif location in ("query", "querystring"): return "query" return location @app.route("/") @parser.use_args({"foo": fields.Int(), "bar": fields.Str()}, location="query") @parser.use_args({"baz": fields.Str()}, location="json") def myview(*, query, body): ... Additionally, this makes it possible to make custom schema classes which provide an argument name. For example, .. code-block:: python from marshmallow import Schema from webargs.flaskparser import FlaskParser class RectangleSchema(Schema): webargs_arg_name = "rectangle" length = fields.Float() width = fields.Float() class MyParser(FlaskParser): USE_ARGS_POSITIONAL = False def get_default_arg_name(self, location, schema): if hasattr(schema, "webargs_arg_name"): if isinstance(schema.webargs_arg_name, str): return schema.webargs_arg_name return super().get_default_arg_name(location, schema) @app.route("/") @parser.use_args({"foo": fields.Int(), "bar": fields.Str()}, location="query") @parser.use_args(RectangleSchema, location="json") def myview(*, rectangle, query_args): ... Next Steps ---------- - See the :doc:`Framework Support ` page for framework-specific guides. - For example applications, check out the `examples `_ directory. webargs-8.5.0/docs/api.rst000066400000000000000000000020771461250545000154210ustar00rootroot00000000000000API === .. module:: webargs webargs.core ------------ .. automodule:: webargs.core :inherited-members: webargs.fields -------------- .. automodule:: webargs.fields :members: Nested, DelimitedList webargs.multidictproxy ---------------------- .. automodule:: webargs.multidictproxy :members: webargs.asyncparser ------------------- .. automodule:: webargs.asyncparser :inherited-members: webargs.flaskparser ------------------- .. automodule:: webargs.flaskparser :members: webargs.djangoparser -------------------- .. automodule:: webargs.djangoparser :members: webargs.bottleparser -------------------- .. automodule:: webargs.bottleparser :members: webargs.tornadoparser --------------------- .. automodule:: webargs.tornadoparser :members: webargs.pyramidparser --------------------- .. automodule:: webargs.pyramidparser :members: webargs.falconparser --------------------- .. automodule:: webargs.falconparser :members: webargs.aiohttpparser --------------------- .. automodule:: webargs.aiohttpparser :members: webargs-8.5.0/docs/authors.rst000066400000000000000000000000351461250545000163250ustar00rootroot00000000000000 .. include:: ../AUTHORS.rst webargs-8.5.0/docs/changelog.rst000066400000000000000000000000361461250545000165700ustar00rootroot00000000000000.. include:: ../CHANGELOG.rst webargs-8.5.0/docs/conf.py000077500000000000000000000026641461250545000154220ustar00rootroot00000000000000import importlib.metadata extensions = [ "sphinx.ext.autodoc", "sphinx.ext.viewcode", "sphinx.ext.intersphinx", "sphinx_issues", ] primary_domain = "py" default_role = "py:obj" github_user = "marshmallow-code" github_repo = "webargs" issues_github_path = f"{github_user}/{github_repo}" intersphinx_mapping = { "python": ("http://python.readthedocs.io/en/latest/", None), "marshmallow": ("http://marshmallow.readthedocs.io/en/latest/", None), } # The master toctree document. master_doc = "index" language = "en" html_domain_indices = False source_suffix = ".rst" project = "webargs" copyright = "Steven Loria and contributors" version = release = importlib.metadata.version("webargs") templates_path = ["_templates"] exclude_patterns = ["_build"] # THEME html_theme = "furo" html_theme_options = { "light_css_variables": {"color-brand-primary": "#268bd2"}, } html_logo = "_static/logo.png" html_context = { "tidelift_url": ( "https://tidelift.com/subscription/pkg/pypi-webargs" "?utm_source=pypi-webargs&utm_medium=referral&utm_campaign=docs" ), "donate_url": "https://opencollective.com/marshmallow", } html_sidebars = { "*": [ "sidebar/scroll-start.html", "sidebar/brand.html", "sidebar/search.html", "sidebar/navigation.html", "donate.html", "sponsors.html", "sidebar/ethical-ads.html", "sidebar/scroll-end.html", ] } webargs-8.5.0/docs/contributing.rst000066400000000000000000000000411461250545000173440ustar00rootroot00000000000000.. include:: ../CONTRIBUTING.rst webargs-8.5.0/docs/ecosystem.rst000066400000000000000000000002331461250545000166530ustar00rootroot00000000000000Ecosystem ========= A list of webargs-related libraries can be found at the GitHub wiki here: https://github.com/marshmallow-code/webargs/wiki/Ecosystem webargs-8.5.0/docs/framework_support.rst000066400000000000000000000321171461250545000204370ustar00rootroot00000000000000.. _frameworks: Framework Support ================= This section includes notes for using webargs with specific web frameworks. Flask ----- Flask support is available via the :mod:`webargs.flaskparser` module. Decorator Usage +++++++++++++++ When using the :meth:`use_args ` decorator, the arguments dictionary will be *before* any URL variable parameters. .. code-block:: python from webargs import fields from webargs.flaskparser import use_args @app.route("/user/") @use_args({"per_page": fields.Int()}, location="query") def user_detail(args, uid): return ("The user page for user {uid}, showing {per_page} posts.").format( uid=uid, per_page=args["per_page"] ) Error Handling ++++++++++++++ Webargs uses Flask's ``abort`` function to raise an ``HTTPException`` when a validation error occurs. If you use the ``Flask.errorhandler`` method to handle errors, you can access validation messages from the ``messages`` attribute of the attached ``ValidationError``. Here is an example error handler that returns validation messages to the client as JSON. .. code-block:: python from flask import jsonify # Return validation errors as JSON @app.errorhandler(422) @app.errorhandler(400) def handle_error(err): headers = err.data.get("headers", None) messages = err.data.get("messages", ["Invalid request."]) if headers: return jsonify({"errors": messages}), err.code, headers else: return jsonify({"errors": messages}), err.code URL Matches +++++++++++ The `FlaskParser` supports parsing values from a request's ``view_args``. .. code-block:: python from webargs.flaskparser import use_args @app.route("/greeting//") @use_args({"name": fields.Str()}, location="view_args") def greeting(args, **kwargs): return "Hello {}".format(args["name"]) Django ------ Django support is available via the :mod:`webargs.djangoparser` module. Webargs can parse Django request arguments in both function-based and class-based views. Decorator Usage +++++++++++++++ When using the :meth:`use_args ` decorator, the arguments dictionary will positioned after the ``request`` argument. **Function-based Views** .. code-block:: python from django.http import HttpResponse from webargs import Arg from webargs.djangoparser import use_args account_args = { "username": fields.Str(required=True), "password": fields.Str(required=True), } @use_args(account_args, location="form") def login_user(request, args): if request.method == "POST": login(args["username"], args["password"]) return HttpResponse("Login page") **Class-based Views** .. code-block:: python from django.views.generic import View from django.shortcuts import render_to_response from webargs import fields from webargs.djangoparser import use_args blog_args = {"title": fields.Str(), "author": fields.Str()} class BlogPostView(View): @use_args(blog_args, location="query") def get(self, request, args): blog_post = Post.objects.get(title__iexact=args["title"], author=args["author"]) return render_to_response("post_template.html", {"post": blog_post}) Error Handling ++++++++++++++ The :class:`DjangoParser` does not override :meth:`handle_error `, so your Django views are responsible for catching any :exc:`ValidationErrors` raised by the parser and returning the appropriate `HTTPResponse`. .. code-block:: python from django.http import JsonResponse from webargs import fields, ValidationError, json argmap = {"name": fields.Str(required=True)} def index(request): try: args = parser.parse(argmap, request) except ValidationError as err: return JsonResponse(err.messages, status=422) except json.JSONDecodeError: return JsonResponse({"json": ["Invalid JSON body."]}, status=400) return JsonResponse({"message": "Hello {name}".format(name=name)}) Tornado ------- Tornado argument parsing is available via the :mod:`webargs.tornadoparser` module. The :class:`webargs.tornadoparser.TornadoParser` parses arguments from a :class:`tornado.httpserver.HTTPRequest` object. The :class:`TornadoParser ` can be used directly, or you can decorate handler methods with :meth:`use_args ` or :meth:`use_kwargs `. .. code-block:: python import tornado.ioloop import tornado.web from webargs import fields from webargs.tornadoparser import parser class HelloHandler(tornado.web.RequestHandler): hello_args = {"name": fields.Str()} def post(self, id): reqargs = parser.parse(self.hello_args, self.request) response = {"message": "Hello {}".format(reqargs["name"])} self.write(response) application = tornado.web.Application([(r"/hello/([0-9]+)", HelloHandler)], debug=True) if __name__ == "__main__": application.listen(8888) tornado.ioloop.IOLoop.instance().start() Decorator Usage +++++++++++++++ When using the :meth:`use_args ` decorator, the decorated method will have the dictionary of parsed arguments passed as a positional argument after ``self`` and any regex match groups from the URL spec. .. code-block:: python from webargs import fields from webargs.tornadoparser import use_args class HelloHandler(tornado.web.RequestHandler): @use_args({"name": fields.Str()}) def post(self, id, reqargs): response = {"message": "Hello {}".format(reqargs["name"])} self.write(response) application = tornado.web.Application([(r"/hello/([0-9]+)", HelloHandler)], debug=True) As with the other parser modules, :meth:`use_kwargs ` will add keyword arguments to the view callable. Error Handling ++++++++++++++ A `HTTPError ` will be raised in the event of a validation error. Your `RequestHandlers` are responsible for handling these errors. Here is how you could write the error messages to a JSON response. .. code-block:: python from tornado.web import RequestHandler class MyRequestHandler(RequestHandler): def write_error(self, status_code, **kwargs): """Write errors as JSON.""" self.set_header("Content-Type", "application/json") if "exc_info" in kwargs: etype, exc, traceback = kwargs["exc_info"] if hasattr(exc, "messages"): self.write({"errors": exc.messages}) if getattr(exc, "headers", None): for name, val in exc.headers.items(): self.set_header(name, val) self.finish() Pyramid ------- Pyramid support is available via the :mod:`webargs.pyramidparser` module. Decorator Usage +++++++++++++++ When using the :meth:`use_args ` decorator on a view callable, the arguments dictionary will be positioned after the `request` argument. .. code-block:: python from pyramid.response import Response from webargs import fields from webargs.pyramidparser import use_args @use_args({"uid": fields.Str(), "per_page": fields.Int()}, location="query") def user_detail(request, args): uid = args["uid"] return Response( "The user page for user {uid}, showing {per_page} posts.".format( uid=uid, per_page=args["per_page"] ) ) As with the other parser modules, :meth:`use_kwargs ` will add keyword arguments to the view callable. URL Matches +++++++++++ The `PyramidParser` supports parsing values from a request's matchdict. .. code-block:: python from pyramid.response import Response from webargs.pyramidparser import use_args @use_args({"mymatch": fields.Int()}, location="matchdict") def matched(request, args): return Response("The value for mymatch is {}".format(args["mymatch"])) Falcon ------ Falcon support is available via the :mod:`webargs.falconparser` module. Decorator Usage +++++++++++++++ When using the :meth:`use_args ` decorator on a resource method, the arguments dictionary will be positioned directly after the request and response arguments. .. code-block:: python import falcon from webargs import fields from webargs.falconparser import use_args class BlogResource: request_args = {"title": fields.Str(required=True)} @use_args(request_args) def on_post(self, req, resp, args, post_id): content = args["title"] # ... api = application = falcon.API() api.add_route("/blogs/{post_id}") As with the other parser modules, :meth:`use_kwargs ` will add keyword arguments to your resource methods. Hook Usage ++++++++++ You can easily implement hooks by using `parser.parse ` directly. .. code-block:: python import falcon from webargs import fields from webargs.falconparser import parser def add_args(argmap, **kwargs): def hook(req, resp, resource, params): parsed_args = parser.parse(argmap, req=req, **kwargs) req.context["args"] = parsed_args return hook @falcon.before(add_args({"page": fields.Int()}, location="query")) class AuthorResource: def on_get(self, req, resp): args = req.context["args"] page = args.get("page") # ... aiohttp ------- aiohttp support is available via the :mod:`webargs.aiohttpparser` module. The `parse ` method of `AIOHTTPParser ` is a `coroutine `. .. code-block:: python import asyncio from aiohttp import web from webargs import fields from webargs.aiohttpparser import parser handler_args = {"name": fields.Str(load_default="World")} async def handler(request): args = await parser.parse(handler_args, request) return web.Response(body="Hello, {}".format(args["name"]).encode("utf-8")) Decorator Usage +++++++++++++++ When using the :meth:`use_args ` decorator on a handler, the parsed arguments dictionary will be the last positional argument. .. code-block:: python import asyncio from aiohttp import web from webargs import fields from webargs.aiohttpparser import use_args @use_args({"content": fields.Str(required=True)}) async def create_comment(request, args): content = args["content"] # ... app = web.Application() app.router.add_route("POST", "/comments/", create_comment) As with the other parser modules, :meth:`use_kwargs ` will add keyword arguments to your resource methods. Usage with coroutines +++++++++++++++++++++ The :meth:`use_args ` and :meth:`use_kwargs ` decorators will work with both `async def` coroutines and generator-based coroutines decorated with `asyncio.coroutine`. .. code-block:: python import asyncio from aiohttp import web from webargs import fields from webargs.aiohttpparser import use_kwargs hello_args = {"name": fields.Str(load_default="World")} # The following are equivalent @asyncio.coroutine @use_kwargs(hello_args) def hello(request, name): return web.Response(body="Hello, {}".format(name).encode("utf-8")) @use_kwargs(hello_args) async def hello(request, name): return web.Response(body="Hello, {}".format(name).encode("utf-8")) URL Matches +++++++++++ The `AIOHTTPParser ` supports parsing values from a request's ``match_info``. .. code-block:: python from aiohttp import web from webargs.aiohttpparser import use_args @parser.use_args({"slug": fields.Str()}, location="match_info") def article_detail(request, args): return web.Response(body="Slug: {}".format(args["slug"]).encode("utf-8")) app = web.Application() app.router.add_route("GET", "/articles/{slug}", article_detail) Bottle ------ Bottle support is available via the :mod:`webargs.bottleparser` module. Decorator Usage +++++++++++++++ The preferred way to apply decorators to Bottle routes is using the ``apply`` argument. .. code-block:: python from bottle import route user_args = {"name": fields.Str(load_default="Friend")} @route("/users/<_id:int>", method="GET", apply=use_args(user_args)) def users(args, _id): """A welcome page.""" return {"message": "Welcome, {}!".format(args["name"]), "_id": _id} webargs-8.5.0/docs/index.rst000066400000000000000000000060421461250545000157530ustar00rootroot00000000000000======= webargs ======= Release v\ |version|. (:doc:`Changelog `) 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. Upgrading from an older version? -------------------------------- See the :doc:`Upgrading to Newer Releases ` page for notes on getting your code up-to-date with the latest version. Usage and Simple Examples ------------------------- .. code-block:: python from flask import Flask from webargs import fields from webargs.flaskparser import use_args app = Flask(__name__) @app.route("/") @use_args({"name": fields.Str(required=True)}, location="query") def index(args): return "Hello " + args["name"] if __name__ == "__main__": app.run() # curl http://localhost:5000/\?name\='World' # Hello World By default Webargs will automatically parse JSON request bodies. But it also has support for: **Query Parameters** :: $ curl http://localhost:5000/\?name\='Freddie' Hello Freddie # pass location="query" to use_args **Form Data** :: $ curl -d 'name=Brian' http://localhost:5000/ Hello Brian # pass location="form" to use_args **JSON Data** :: $ curl -X POST -H "Content-Type: application/json" -d '{"name":"Roger"}' http://localhost:5000/ Hello Roger # pass location="json" (or omit location) to use_args and, optionally: - Headers - Cookies - Files - Paths Why Use It ---------- * **Simple, declarative syntax**. Define your arguments as a mapping rather than imperatively pulling values off of request objects. * **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. * **Self-documentation**. Webargs makes it easy to understand the expected arguments and their types for your view functions. * **Automatic documentation**. The metadata that webargs provides can serve as an aid for automatically generating API documentation. * **Cross-framework compatibility**. Webargs provides a consistent request-parsing interface that will work across many Python web frameworks. * **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. Get It Now ---------- :: pip install -U webargs Ready to get started? Go on to the :doc:`Quickstart tutorial ` or check out some `examples `_. User Guide ---------- .. toctree:: :maxdepth: 2 install quickstart advanced framework_support ecosystem API Reference ------------- .. toctree:: :maxdepth: 2 api Project Info ------------ .. toctree:: :maxdepth: 1 license changelog upgrading authors contributing webargs-8.5.0/docs/install.rst000066400000000000000000000007001461250545000163050ustar00rootroot00000000000000Install ======= **webargs** requires Python >= 3.8. It depends on `marshmallow `_ >= 3.0.0. From the PyPI ------------- To install the latest version from the PyPI: :: $ pip install -U webargs Get the Bleeding Edge Version ----------------------------- To get the latest development version of webargs, run :: $ pip install -U git+https://github.com/marshmallow-code/webargs.git@dev webargs-8.5.0/docs/license.rst000066400000000000000000000000711461250545000162620ustar00rootroot00000000000000 ******* License ******* .. literalinclude:: ../LICENSE webargs-8.5.0/docs/make.bat000066400000000000000000000145021461250545000155170ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\complexity.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\complexity.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :endwebargs-8.5.0/docs/quickstart.rst000066400000000000000000000165401461250545000170420ustar00rootroot00000000000000Quickstart ========== Basic Usage ----------- Arguments are specified as a dictionary of name -> :class:`Field ` pairs. .. code-block:: python from webargs import fields, validate user_args = { # Required arguments "username": fields.Str(required=True), # Validation "password": fields.Str(validate=lambda p: len(p) >= 6), # OR use marshmallow's built-in validators "password": fields.Str(validate=validate.Length(min=6)), # Default value when argument is missing "display_per_page": fields.Int(load_default=10), # Repeated parameter, e.g. "/?nickname=Fred&nickname=Freddie" "nickname": fields.List(fields.Str()), # Delimited list, e.g. "/?languages=python,javascript" "languages": fields.DelimitedList(fields.Str()), # When value is keyed on a variable-unsafe name # or you want to rename a key "user_type": fields.Str(data_key="user-type"), } .. note:: See the `marshmallow.fields` documentation for a full reference on available field types. To parse request arguments, use the :meth:`parse ` method of a :class:`Parser ` object. .. code-block:: python from flask import request from webargs.flaskparser import parser @app.route("/register", methods=["POST"]) def register(): args = parser.parse(user_args, request) return register_user( args["username"], args["password"], fullname=args["fullname"], per_page=args["display_per_page"], ) Decorator API ------------- 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. .. code-block:: python from webargs.flaskparser import use_args, use_kwargs @app.route("/register", methods=["POST"]) @use_args(user_args) # Injects args dictionary def register(args): return register_user( args["username"], args["password"], fullname=args["fullname"], per_page=args["display_per_page"], ) @app.route("/settings", methods=["POST"]) @use_kwargs(user_args) # Injects keyword arguments def user_settings(username, password, fullname, display_per_page, nickname): return render_template("settings.html", username=username, nickname=nickname) .. note:: When using `use_kwargs`, any missing values will be omitted from the arguments. Use ``**kwargs`` to handle optional arguments. .. code-block:: python from webargs import fields, missing @use_kwargs({"name": fields.Str(required=True), "nickname": fields.Str(required=False)}) def myview(name, **kwargs): if "nickname" not in kwargs: # ... pass Request "Locations" ------------------- 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: .. code-block:: python @app.route("/register") @use_args(user_args, location="form") def register(args): return "registration page" Available locations include: - ``'querystring'`` (same as ``'query'``) - ``'json'`` - ``'form'`` - ``'headers'`` - ``'cookies'`` - ``'files'`` Validation ---------- Each :class:`Field ` object can be validated individually by passing the ``validate`` argument. .. code-block:: python from webargs import fields args = {"age": fields.Int(validate=lambda val: val > 0)} The validator may return either a `boolean` or raise a :exc:`ValidationError `. .. code-block:: python from webargs import fields, ValidationError def must_exist_in_db(val): if not User.query.get(val): # Optionally pass a status_code raise ValidationError("User does not exist") args = {"id": fields.Int(validate=must_exist_in_db)} .. note:: If a validator returns ``None``, validation will pass. A validator must return ``False`` or raise a `ValidationError ` for validation to fail. There are a number of built-in validators from `marshmallow.validate ` (re-exported as `webargs.validate`). .. code-block:: python from webargs import fields, validate args = { "name": fields.Str(required=True, validate=[validate.Length(min=1, max=9999)]), "age": fields.Int(validate=[validate.Range(min=1, max=999)]), } The full arguments dictionary can also be validated by passing ``validate`` to :meth:`Parser.parse `, :meth:`Parser.use_args `, :meth:`Parser.use_kwargs `. .. code-block:: python from webargs import fields from webargs.flaskparser import parser argmap = {"age": fields.Int(), "years_employed": fields.Int()} # ... result = parser.parse( argmap, validate=lambda args: args["years_employed"] < args["age"] ) Error Handling -------------- Each parser has a default error handling method. To override the error handling callback, write a function that receives an error, the request, the `marshmallow.Schema` instance, status code, and headers. Then decorate that function with :func:`Parser.error_handler `. .. code-block:: python from webargs.flaskparser import parser class CustomError(Exception): pass @parser.error_handler def handle_error(error, req, schema, *, error_status_code, error_headers): raise CustomError(error.messages) Parsing Lists in Query Strings ------------------------------ Use `fields.DelimitedList ` to parse comma-separated lists in query parameters, e.g. ``/?permissions=read,write`` .. code-block:: python from webargs import fields args = {"permissions": fields.DelimitedList(fields.Str())} If you expect repeated query parameters, e.g. ``/?repo=webargs&repo=marshmallow``, use `fields.List ` instead. .. code-block:: python from webargs import fields args = {"repo": fields.List(fields.Str())} Nesting Fields -------------- :class:`Field ` dictionaries can be nested within each other. This can be useful for validating nested data. .. code-block:: python from webargs import fields args = { "name": fields.Nested( {"first": fields.Str(required=True), "last": fields.Str(required=True)} ) } .. note:: 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. Next Steps ---------- - Go on to :doc:`Advanced Usage ` to learn how to add custom location handlers, use marshmallow Schemas, and more. - See the :doc:`Framework Support ` page for framework-specific guides. - For example applications, check out the `examples `_ directory. webargs-8.5.0/docs/upgrading.rst000066400000000000000000000412001461250545000166170ustar00rootroot00000000000000Upgrading to Newer Releases =========================== This section documents migration paths to new releases. Upgrading to 8.0 ++++++++++++++++ In 8.0, the default values for ``unknown`` were changed. When the location is set to ``json``, ``form``, or ``json_or_form``, the default for ``unknown`` is now ``None``. Previously, the default was ``RAISE``. Because ``RAISE`` is the default value for ``unknown`` on marshmallow schemas, this change only affects usage in which the following conditions are met: * A schema with ``unknown`` set to ``INCLUDE`` or ``EXCLUDE`` is passed to webargs ``use_args``, ``use_kwargs``, or ``parse`` * ``unknown`` is not passed explicitly to the webargs function * ``location`` is not set (default of ``json``) or is set explicitly to ``json``, ``form``, or ``json_or__form`` For example .. code-block:: python import marshmallow as ma class BodySchema(ma.Schema): foo = ma.fields.String() class Meta: unknown = ma.EXCLUDE @parser.use_args(BodySchema) def foo(data): ... In this case, under webargs 7.0 the schema ``unknown`` setting of ``EXCLUDE`` would be ignored. Instead, ``unknown=RAISE`` would be used. In webargs 8.0, the schema ``unknown`` is used. To get the webargs 7.0 behavior (overriding the Schema ``unknown``), simply pass ``unknown`` to ``use_args``, as in .. code-block:: python @parser.use_args(BodySchema, unknown=ma.RAISE) def foo(data): ... Upgrading to 7.0 ++++++++++++++++ `unknown` is Now Settable by the Parser --------------------------------------- As of 7.0, `Parsers` have multiple settings for controlling the value for `unknown` which is passed to `schema.load` when parsing. To set unknown behavior on a parser, see the advanced doc on this topic: :ref:`advanced_setting_unknown`. Importantly, by default, any schema setting for `unknown` will be overridden by the `unknown` settings for the parser. In order to use a schema's `unknown` value, set `unknown=None` on the parser. In 6.x versions of webargs, schema values for `unknown` are used, so the `unknown=None` setting is the best way to emulate this. To get identical behavior: .. code-block:: python # assuming you have a schema named MySchema # webargs 6.x @parser.use_args(MySchema) def foo(args): ... # webargs 7.x # as a parameter to use_args or parse @parser.use_args(MySchema, unknown=None) def foo(args): ... # webargs 7.x # as a parser setting # example with flaskparser, but any parser class works parser = FlaskParser(unknown=None) @parser.use_args(MySchema) def foo(args): ... Upgrading to 6.0 ++++++++++++++++ Multiple Locations Are No Longer Supported In A Single Call ----------------------------------------------------------- The default location is JSON/body. Under webargs 5.x, code often did not have to specify a location. Because webargs would parse data from multiple locations automatically, users did not need to specify where a parameter, call it `q`, was passed. `q` could be in a query parameter or in a JSON or form-post body. Now, webargs requires that users specify only one location for data loading per `use_args` call, and `"json"` is the default. If `q` is intended to be a query parameter, the developer must be explicit and rewrite like so: .. code-block:: python # webargs 5.x @parser.use_args({"q": ma.fields.String()}) def foo(args): return some_function(user_query=args.get("q")) # webargs 6.x @parser.use_args({"q": ma.fields.String()}, location="query") def foo(args): return some_function(user_query=args.get("q")) This also means that another usage from 5.x is not supported. Code with multiple locations in a single `use_args`, `use_kwargs`, or `parse` call must be rewritten in multiple separate `use_args` or `use_kwargs` invocations, like so: .. code-block:: python # webargs 5.x @parser.use_kwargs( { "q1": ma.fields.Int(location="query"), "q2": ma.fields.Int(location="query"), "h1": ma.fields.Int(location="headers"), }, locations=("query", "headers"), ) def foo(q1, q2, h1): ... # webargs 6.x @parser.use_kwargs({"q1": ma.fields.Int(), "q2": ma.fields.Int()}, location="query") @parser.use_kwargs({"h1": ma.fields.Int()}, location="headers") def foo(q1, q2, h1): ... Fields No Longer Support location=... ------------------------------------- Because a single `parser.use_args`, `parser.use_kwargs`, or `parser.parse` call cannot specify multiple locations, it is not necessary for a field to be able to specify its location. Rewrite code like so: .. code-block:: python # webargs 5.x @parser.use_args({"q": ma.fields.String(location="query")}) def foo(args): return some_function(user_query=args.get("q")) # webargs 6.x @parser.use_args({"q": ma.fields.String()}, location="query") def foo(args): return some_function(user_query=args.get("q")) location_handler Has Been Replaced With location_loader ------------------------------------------------------- This is not just a name change. The expected signature of a `location_loader` is slightly different from the signature for a `location_handler`. Where previously a `location_handler` code took the incoming request data and details of a single field being loaded, a `location_loader` takes the request and the schema as a pair. It does not return a specific field's data, but data for the whole location. Rewrite code like this: .. code-block:: python # webargs 5.x @parser.location_handler("data") def load_data(request, name, field): return request.data.get(name) # webargs 6.x @parser.location_loader("data") def load_data(request, schema): return request.data Data Is Not Filtered Before Being Passed To Schemas, And It May Be Proxified ---------------------------------------------------------------------------- In webargs 5.x, the deserialization schema was used to pull data out of the request object. That data was compiled into a dictionary which was then passed to the schema. One of the major changes in webargs 6.x allows the use of `unknown` parameter on schemas. This lets a schema decide what to do with fields not specified in the schema. In order to achieve this, webargs now passes the full data from the specified location to the schema. Therefore, users should specify `unknown=marshmallow.EXCLUDE` on their schemas in order to filter out unknown fields. Like so: .. code-block:: python # webargs 5.x # this can assume that "q" is the only parameter passed, and all other # parameters will be ignored @parser.use_kwargs({"q": ma.fields.String()}, locations=("query",)) def foo(q): ... # webargs 6.x, Solution 1: declare a schema with Meta.unknown set class QuerySchema(ma.Schema): q = ma.fields.String() class Meta: unknown = ma.EXCLUDE @parser.use_kwargs(QuerySchema, location="query") def foo(q): ... # webargs 6.x, Solution 2: instantiate a schema with unknown set class QuerySchema(ma.Schema): q = ma.fields.String() @parser.use_kwargs(QuerySchema(unknown=ma.EXCLUDE), location="query") def foo(q): ... This also allows usage which passes the unknown parameters through, like so: .. code-block:: python # webargs 6.x only! cannot be done in 5.x class QuerySchema(ma.Schema): q = ma.fields.String() # will pass *all* query params through as "kwargs" @parser.use_kwargs(QuerySchema(unknown=ma.INCLUDE), location="query") def foo(q, **kwargs): ... However, many types of request data are so-called "multidicts" -- dictionary-like types which can return one or multiple values. To handle `marshmallow.fields.List` and `webargs.fields.DelimitedList` fields correctly, passing list data, webargs must combine schema information with the raw request data. This is done in the :class:`MultiDictProxy ` type, which will often be passed to schemas. This means that if a schema has a `pre_load` hook which interacts with the data, it may need modifications. For example, a `flask` query string will be parsed into an `ImmutableMultiDict` type, which will break pre-load hooks which modify the data in-place. Such usages need rewrites like so: .. code-block:: python # webargs 5.x # flask query params is just an example -- applies to several types from webargs.flaskparser import use_kwargs class QuerySchema(ma.Schema): q = ma.fields.String() @ma.pre_load def convert_nil_to_none(self, obj, **kwargs): if obj.get("q") == "nil": obj["q"] = None return obj @use_kwargs(QuerySchema, locations=("query",)) def foo(q): ... # webargs 6.x class QuerySchema(ma.Schema): q = ma.fields.String() # unlike under 5.x, we cannot modify 'obj' in-place because writing # to the MultiDictProxy will try to write to the underlying # ImmutableMultiDict, which is not allowed @ma.pre_load def convert_nil_to_none(self, obj, **kwargs): # creating a dict from a MultiDictProxy works well because it # "unwraps" lists and delimited lists correctly data = dict(obj) if data.get("q") == "nil": data["q"] = None return data @parser.use_kwargs(QuerySchema, location="query") def foo(q): ... DelimitedList Now Only Takes A String Input ------------------------------------------- Combining `List` and string parsing functionality in a single type had some messy corner cases. For the most part, this should not require rewrites. But for APIs which need to allow both usages, rewrites are possible like so: .. code-block:: python # webargs 5.x # this allows ...?x=1&x=2&x=3 # as well as ...?x=1,2,3 @use_kwargs({"x": webargs.fields.DelimitedList(ma.fields.Int)}, locations=("query",)) def foo(x): ... # webargs 6.x # this accepts x=1,2,3 but NOT x=1&x=2&x=3 @use_kwargs({"x": webargs.fields.DelimitedList(ma.fields.Int)}, location="query") def foo(x): ... # webargs 6.x # this accepts x=1,2,3 ; x=1&x=2&x=3 ; x=1,2&x=3 # to do this, it needs a post_load hook which will flatten out the list data class UnpackingDelimitedListSchema(ma.Schema): x = ma.fields.List(webargs.fields.DelimitedList(ma.fields.Int)) @ma.post_load def flatten_lists(self, data, **kwargs): new_x = [] for x in data["x"]: new_x.extend(x) data["x"] = new_x return data @parser.use_kwargs(UnpackingDelimitedListSchema, location="query") def foo(x): ... ValidationError Messages Are Namespaced Under The Location ---------------------------------------------------------- Code parsing ValidationError messages will notice a change in the messages produced by webargs. What would previously have come back with messages like `{"foo":["Not a valid integer."]}` will now have messages nested one layer deeper, like `{"json":{"foo":["Not a valid integer."]}}`. To rewrite code which was handling these errors, the handler will need to be prepared to traverse messages by one additional level. For example: .. code-block:: python import logging log = logging.getLogger(__name__) # webargs 5.x # logs debug messages like # bad value for 'foo': ["Not a valid integer."] # bad value for 'bar': ["Not a valid boolean."] def log_invalid_parameters(validation_error): for field, messages in validation_error.messages.items(): log.debug("bad value for '{}': {}".format(field, messages)) # webargs 6.x # logs debug messages like # bad value for 'foo' [query]: ["Not a valid integer."] # bad value for 'bar' [json]: ["Not a valid boolean."] def log_invalid_parameters(validation_error): for location, fielddata in validation_error.messages.items(): for field, messages in fielddata.items(): log.debug("bad value for '{}' [{}]: {}".format(field, location, messages)) Custom Error Handler Argument Names Changed ------------------------------------------- If you define a custom error handler via `@parser.error_handler` the function arguments are now keyword-only and `status_code` and `headers` have been renamed `error_status_code` and `error_headers`. .. code-block:: python # webargs 5.x @parser.error_handler def custom_handle_error(error, req, schema, status_code, headers): ... # webargs 6.x @parser.error_handler def custom_handle_error(error, req, schema, *, error_status_code, error_headers): ... Some Functions Take Keyword-Only Arguments Now ---------------------------------------------- The signature of several methods has changed to have keyword-only arguments. For the most part, this should not require any changes, but here's a list of the changes. `parser.error_handler` methods: .. code-block:: python # webargs 5.x def handle_error(error, req, schema, status_code, headers): ... # webargs 6.x def handle_error(error, req, schema, *, error_status_code, error_headers): ... `parser.__init__` methods: .. code-block:: python # webargs 5.x def __init__(self, location=None, error_handler=None, schema_class=None): ... # webargs 6.x def __init__(self, location=None, *, error_handler=None, schema_class=None): ... `parser.parse`, `parser.use_args`, and `parser.use_kwargs` methods: .. code-block:: python # webargs 5.x def parse( self, argmap, req=None, location=None, validate=None, error_status_code=None, error_headers=None, ): ... # webargs 6.x def parse( self, argmap, req=None, *, location=None, validate=None, error_status_code=None, error_headers=None, ): ... # webargs 5.x def use_args( self, argmap, req=None, location=None, as_kwargs=False, validate=None, error_status_code=None, error_headers=None, ): ... # webargs 6.x def use_args( self, argmap, req=None, *, location=None, as_kwargs=False, validate=None, error_status_code=None, error_headers=None, ): ... # use_kwargs is just an alias for use_args with as_kwargs=True and finally, the `dict2schema` function: .. code-block:: python # webargs 5.x def dict2schema(dct, schema_class=ma.Schema): ... # webargs 6.x def dict2schema(dct, *, schema_class=ma.Schema): ... PyramidParser Now Appends Arguments (Used To Prepend) ----------------------------------------------------- `PyramidParser.use_args` was not conformant with the other parsers in webargs. While all other parsers added new arguments to the end of the argument list of a decorated view function, the Pyramid implementation added them to the front of the argument list. This has been corrected, but as a result pyramid views with `use_args` may need to be rewritten. The `request` object is always passed first in both versions, so the issue is only apparent with view functions taking other positional arguments. For example, imagine code with a decorator for passing user information, `pass_userinfo`, like so: .. code-block:: python # a decorator which gets information about the authenticated user def pass_userinfo(f): def decorator(request, *args, **kwargs): return f(request, get_userinfo(), *args, **kwargs) return decorator You will see a behavioral change if `pass_userinfo` is called on a function decorated with `use_args`. The difference between the two versions will be like so: .. code-block:: python from webargs.pyramidparser import use_args # webargs 5.x # pass_userinfo is called first, webargs sees positional arguments of # (userinfo,) # and changes it to # (request, args, userinfo) @pass_userinfo @use_args({"q": ma.fields.String()}, locations=("query",)) def viewfunc(request, args, userinfo): q = args.get("q") ... # webargs 6.x # pass_userinfo is called first, webargs sees positional arguments of # (userinfo,) # and changes it to # (request, userinfo, args) @pass_userinfo @use_args({"q": ma.fields.String()}, location="query") def viewfunc(request, userinfo, args): q = args.get("q") ... webargs-8.5.0/examples/000077500000000000000000000000001461250545000147765ustar00rootroot00000000000000webargs-8.5.0/examples/__init__.py000066400000000000000000000000001461250545000170750ustar00rootroot00000000000000webargs-8.5.0/examples/aiohttp_example.py000066400000000000000000000046311461250545000205370ustar00rootroot00000000000000"""A simple number and datetime addition JSON API. Run the app: $ python examples/aiohttp_example.py Try the following with httpie (a cURL-like utility, http://httpie.org): $ pip install httpie $ http GET :5001/ $ http GET :5001/ name==Ada $ http POST :5001/add x=40 y=2 $ http POST :5001/dateadd value=1973-04-10 addend=63 $ http POST :5001/dateadd value=2014-10-23 addend=525600 unit=minutes """ import asyncio import datetime as dt from aiohttp import web from aiohttp.web import json_response from webargs import fields, validate from webargs.aiohttpparser import use_args, use_kwargs hello_args = {"name": fields.Str(load_default="Friend")} @use_args(hello_args) async def index(request, args): """A welcome page.""" return json_response({"message": "Welcome, {}!".format(args["name"])}) add_args = {"x": fields.Float(required=True), "y": fields.Float(required=True)} @use_kwargs(add_args) async def add(request, x, y): """An addition endpoint.""" return json_response({"result": x + y}) dateadd_args = { "value": fields.Date(required=False), "addend": fields.Int(required=True, validate=validate.Range(min=1)), "unit": fields.Str( load_default="days", validate=validate.OneOf(["minutes", "days"]) ), } @use_kwargs(dateadd_args) async def dateadd(request, value, addend, unit): """A datetime adder endpoint.""" value = value or dt.datetime.utcnow() if unit == "minutes": delta = dt.timedelta(minutes=addend) else: delta = dt.timedelta(days=addend) result = value + delta return json_response({"result": result.isoformat()}) def create_app(): app = web.Application() app.router.add_route("GET", "/", index) app.router.add_route("POST", "/add", add) app.router.add_route("POST", "/dateadd", dateadd) return app def run(app, port=5001): loop = asyncio.get_event_loop() handler = app.make_handler() f = loop.create_server(handler, "0.0.0.0", port) srv = loop.run_until_complete(f) print("serving on", srv.sockets[0].getsockname()) try: loop.run_forever() except KeyboardInterrupt: pass finally: loop.run_until_complete(handler.finish_connections(1.0)) srv.close() loop.run_until_complete(srv.wait_closed()) loop.run_until_complete(app.finish()) loop.close() if __name__ == "__main__": app = create_app() run(app) webargs-8.5.0/examples/annotations_example.py000066400000000000000000000073361461250545000214310ustar00rootroot00000000000000"""Example of using Python 3 function annotations to define request arguments and output schemas. Run the app: $ python examples/annotations_example.py Try the following with httpie (a cURL-like utility, http://httpie.org): $ pip install httpie $ http GET :5001/ $ http GET :5001/ name==Ada $ http POST :5001/add x=40 y=2 $ http GET :5001/users/42 """ import functools import random from flask import Flask, request from marshmallow import Schema from webargs import fields from webargs.flaskparser import parser app = Flask(__name__) ##### Routing wrapper #### def route(*args, **kwargs): """Combines `Flask.route` and webargs parsing. Allows arguments to be specified as function annotations. An output schema can optionally be specified by a return annotation. """ def decorator(func): @app.route(*args, **kwargs) @functools.wraps(func) def wrapped_view(*a, **kw): annotations = getattr(func, "__annotations__", {}) reqargs = { name: value for name, value in annotations.items() if isinstance(value, fields.Field) and name != "return" } response_schema = annotations.get("return") schema_cls = Schema.from_dict(reqargs) partial = request.method != "POST" parsed = parser.parse(schema_cls(partial=partial), request) kw.update(parsed) response_data = func(*a, **kw) if response_schema: return response_schema.dump(response_data) else: return func(*a, **kw) return wrapped_view return decorator ##### Fake database and model ##### class Model: def __init__(self, **kwargs): self.__dict__.update(kwargs) def update(self, **kwargs): self.__dict__.update(kwargs) @classmethod def insert(cls, db, **kwargs): collection = db[cls.collection] new_id = None if "id" in kwargs: # for setting up fixtures new_id = kwargs.pop("id") else: # find a new id found_id = False while not found_id: new_id = random.randint(1, 9999) if new_id not in collection: found_id = True new_record = cls(id=new_id, **kwargs) collection[new_id] = new_record return new_record class User(Model): collection = "users" db = {"users": {}} ##### Views ##### @route("/", methods=["GET"]) def index(name: fields.Str(load_default="Friend")): # noqa: F821 return {"message": f"Hello, {name}!"} @route("/add", methods=["POST"]) def add(x: fields.Float(required=True), y: fields.Float(required=True)): return {"result": x + y} class UserSchema(Schema): id = fields.Int(dump_only=True) username = fields.Str(required=True) first_name = fields.Str() last_name = fields.Str() @route("/users/", methods=["GET", "PATCH"]) def user_detail(user_id, username: fields.Str(required=True) = None) -> UserSchema(): user = db["users"].get(user_id) if not user: return {"message": "User not found"}, 404 if request.method == "PATCH": user.update(username=username) return user # Return validation errors as JSON @app.errorhandler(422) @app.errorhandler(400) def handle_error(err): headers = err.data.get("headers", None) messages = err.data.get("messages", ["Invalid request."]) if headers: return {"errors": messages}, err.code, headers else: return {"errors": messages}, err.code if __name__ == "__main__": User.insert( db=db, id=42, username="fred", first_name="Freddie", last_name="Mercury" ) app.run(port=5001, debug=True) webargs-8.5.0/examples/bottle_example.py000066400000000000000000000035061461250545000203600ustar00rootroot00000000000000"""A simple number and datetime addition JSON API. Run the app: $ python examples/bottle_example.py Try the following with httpie (a cURL-like utility, http://httpie.org): $ pip install httpie $ http GET :5001/ $ http GET :5001/ name==Ada $ http POST :5001/add x=40 y=2 $ http POST :5001/dateadd value=1973-04-10 addend=63 $ http POST :5001/dateadd value=2014-10-23 addend=525600 unit=minutes """ import datetime as dt from bottle import error, response, route, run from webargs import fields, validate from webargs.bottleparser import use_args, use_kwargs hello_args = {"name": fields.Str(load_default="Friend")} @route("/", method="GET", apply=use_args(hello_args)) def index(args): """A welcome page.""" return {"message": "Welcome, {}!".format(args["name"])} add_args = {"x": fields.Float(required=True), "y": fields.Float(required=True)} @route("/add", method="POST", apply=use_kwargs(add_args)) def add(x, y): """An addition endpoint.""" return {"result": x + y} dateadd_args = { "value": fields.Date(required=False), "addend": fields.Int(required=True, validate=validate.Range(min=1)), "unit": fields.Str( load_default="days", validate=validate.OneOf(["minutes", "days"]) ), } @route("/dateadd", method="POST", apply=use_kwargs(dateadd_args)) def dateadd(value, addend, unit): """A date adder endpoint.""" value = value or dt.datetime.utcnow() if unit == "minutes": delta = dt.timedelta(minutes=addend) else: delta = dt.timedelta(days=addend) result = value + delta return {"result": result.isoformat()} # Return validation errors as JSON @error(400) @error(422) def handle_error(err): response.content_type = "application/json" return err.body if __name__ == "__main__": run(port=5001, reloader=True, debug=True) webargs-8.5.0/examples/falcon_example.py000066400000000000000000000050521461250545000203270ustar00rootroot00000000000000"""A simple number and datetime addition JSON API. Demonstrates different strategies for parsing arguments with the FalconParser. Run the app: $ pip install gunicorn $ gunicorn examples.falcon_example:app Try the following with httpie (a cURL-like utility, http://httpie.org): $ pip install httpie $ http GET :8000/ $ http GET :8000/ name==Ada $ http POST :8000/add x=40 y=2 $ http POST :8000/dateadd value=1973-04-10 addend=63 $ http POST :8000/dateadd value=2014-10-23 addend=525600 unit=minutes """ import datetime as dt import falcon from webargs import fields, validate from webargs.core import json from webargs.falconparser import parser, use_args, use_kwargs ### Middleware and hooks ### class JSONTranslator: def process_response(self, req, resp, resource): if "result" not in req.context: return resp.body = json.dumps(req.context["result"]) def add_args(argmap, **kwargs): def hook(req, resp, params): req.context["args"] = parser.parse(argmap, req=req, **kwargs) return hook ### Resources ### class HelloResource: """A welcome page.""" hello_args = {"name": fields.Str(load_default="Friend", location="query")} @use_args(hello_args) def on_get(self, req, resp, args): req.context["result"] = {"message": "Welcome, {}!".format(args["name"])} class AdderResource: """An addition endpoint.""" adder_args = {"x": fields.Float(required=True), "y": fields.Float(required=True)} @use_kwargs(adder_args) def on_post(self, req, resp, x, y): req.context["result"] = {"result": x + y} class DateAddResource: """A datetime adder endpoint.""" dateadd_args = { "value": fields.Date(required=False), "addend": fields.Int(required=True, validate=validate.Range(min=1)), "unit": fields.Str( load_default="days", validate=validate.OneOf(["minutes", "days"]) ), } @falcon.before(add_args(dateadd_args)) def on_post(self, req, resp): """A datetime adder endpoint.""" args = req.context["args"] value = args["value"] or dt.datetime.utcnow() if args["unit"] == "minutes": delta = dt.timedelta(minutes=args["addend"]) else: delta = dt.timedelta(days=args["addend"]) result = value + delta req.context["result"] = {"result": result.isoformat()} app = falcon.API(middleware=[JSONTranslator()]) app.add_route("/", HelloResource()) app.add_route("/add", AdderResource()) app.add_route("/dateadd", DateAddResource()) webargs-8.5.0/examples/flask_example.py000066400000000000000000000044011461250545000201620ustar00rootroot00000000000000"""A simple number and datetime addition JSON API. Run the app: $ python examples/flask_example.py Try the following with httpie (a cURL-like utility, http://httpie.org): $ pip install httpie $ http GET :5001/ $ http GET :5001/ name==Ada $ http POST :5001/add x=40 y=2 $ http POST :5001/subtract x=40 y=2 $ http POST :5001/dateadd value=1973-04-10 addend=63 $ http POST :5001/dateadd value=2014-10-23 addend=525600 unit=minutes """ import datetime as dt from flask import Flask, jsonify from webargs import fields, validate from webargs.flaskparser import use_args, use_kwargs app = Flask(__name__) hello_args = {"name": fields.Str(load_default="Friend")} @app.route("/", methods=["GET"]) @use_args(hello_args) def index(args): """A welcome page.""" return jsonify({"message": "Welcome, {}!".format(args["name"])}) add_args = {"x": fields.Float(required=True), "y": fields.Float(required=True)} @app.route("/add", methods=["POST"]) @use_kwargs(add_args) def add(x, y): """An addition endpoint.""" return jsonify({"result": x + y}) @app.route("/subtract", methods=["POST"]) @use_kwargs(add_args) async def subtract(x, y): """An async subtraction endpoint.""" return jsonify({"result": x - y}) dateadd_args = { "value": fields.Date(required=False), "addend": fields.Int(required=True, validate=validate.Range(min=1)), "unit": fields.Str( load_default="days", validate=validate.OneOf(["minutes", "days"]) ), } @app.route("/dateadd", methods=["POST"]) @use_kwargs(dateadd_args) def dateadd(value, addend, unit): """A date adder endpoint.""" value = value or dt.datetime.utcnow() if unit == "minutes": delta = dt.timedelta(minutes=addend) else: delta = dt.timedelta(days=addend) result = value + delta return jsonify({"result": result.isoformat()}) # Return validation errors as JSON @app.errorhandler(422) @app.errorhandler(400) def handle_error(err): headers = err.data.get("headers", None) messages = err.data.get("messages", ["Invalid request."]) if headers: return jsonify({"errors": messages}), err.code, headers else: return jsonify({"errors": messages}), err.code if __name__ == "__main__": app.run(port=5001, debug=True) webargs-8.5.0/examples/flaskrestful_example.py000066400000000000000000000045361461250545000216000ustar00rootroot00000000000000"""A simple number and datetime addition JSON API. Run the app: $ python examples/flaskrestful_example.py Try the following with httpie (a cURL-like utility, http://httpie.org): $ pip install httpie $ http GET :5001/ $ http GET :5001/ name==Ada $ http POST :5001/add x=40 y=2 $ http POST :5001/dateadd value=1973-04-10 addend=63 $ http POST :5001/dateadd value=2014-10-23 addend=525600 unit=minutes """ import datetime as dt from flask import Flask from flask_restful import Api, Resource from webargs import fields, validate from webargs.flaskparser import abort, parser, use_args, use_kwargs app = Flask(__name__) api = Api(app) class IndexResource(Resource): """A welcome page.""" hello_args = {"name": fields.Str(load_default="Friend")} @use_args(hello_args) def get(self, args): return {"message": "Welcome, {}!".format(args["name"])} class AddResource(Resource): """An addition endpoint.""" add_args = {"x": fields.Float(required=True), "y": fields.Float(required=True)} @use_kwargs(add_args) def post(self, x, y): """An addition endpoint.""" return {"result": x + y} class DateAddResource(Resource): dateadd_args = { "value": fields.Date(required=False), "addend": fields.Int(required=True, validate=validate.Range(min=1)), "unit": fields.Str( load_default="days", validate=validate.OneOf(["minutes", "days"]) ), } @use_kwargs(dateadd_args) def post(self, value, addend, unit): """A date adder endpoint.""" value = value or dt.datetime.utcnow() if unit == "minutes": delta = dt.timedelta(minutes=addend) else: delta = dt.timedelta(days=addend) result = value + delta return {"result": result.isoformat()} # This error handler is necessary for usage with Flask-RESTful @parser.error_handler def handle_request_parsing_error(err, req, schema, *, error_status_code, error_headers): """webargs error handler that uses Flask-RESTful's abort function to return a JSON error response to the client. """ abort(error_status_code, errors=err.messages) if __name__ == "__main__": api.add_resource(IndexResource, "/") api.add_resource(AddResource, "/add") api.add_resource(DateAddResource, "/dateadd") app.run(port=5001, debug=True) webargs-8.5.0/examples/pyramid_example.py000066400000000000000000000045211461250545000205320ustar00rootroot00000000000000"""A simple number and datetime addition JSON API. Run the app: $ python examples/pyramid_example.py Try the following with httpie (a cURL-like utility, http://httpie.org): $ pip install httpie $ http GET :5001/ $ http GET :5001/ name==Ada $ http POST :5001/add x=40 y=2 $ http POST :5001/dateadd value=1973-04-10 addend=63 $ http POST :5001/dateadd value=2014-10-23 addend=525600 unit=minutes """ import datetime as dt from wsgiref.simple_server import make_server from pyramid.config import Configurator from pyramid.renderers import JSON from pyramid.view import view_config from webargs import fields, validate from webargs.pyramidparser import use_args, use_kwargs hello_args = {"name": fields.Str(load_default="Friend")} @view_config(route_name="hello", request_method="GET", renderer="json") @use_args(hello_args) def index(request, args): """A welcome page.""" return {"message": "Welcome, {}!".format(args["name"])} add_args = {"x": fields.Float(required=True), "y": fields.Float(required=True)} @view_config(route_name="add", request_method="POST", renderer="json") @use_kwargs(add_args) def add(request, x, y): """An addition endpoint.""" return {"result": x + y} dateadd_args = { "value": fields.Date(required=False), "addend": fields.Int(required=True, validate=validate.Range(min=1)), "unit": fields.Str( load_default="days", validate=validate.OneOf(["minutes", "days"]) ), } @view_config(route_name="dateadd", request_method="POST", renderer="json") @use_kwargs(dateadd_args) def dateadd(request, value, addend, unit): """A date adder endpoint.""" value = value or dt.datetime.utcnow() if unit == "minutes": delta = dt.timedelta(minutes=addend) else: delta = dt.timedelta(days=addend) result = value + delta return {"result": result} if __name__ == "__main__": config = Configurator() json_renderer = JSON() json_renderer.add_adapter(dt.datetime, lambda v, request: v.isoformat()) config.add_renderer("json", json_renderer) config.add_route("hello", "/") config.add_route("add", "/add") config.add_route("dateadd", "/dateadd") config.scan(__name__) app = config.make_wsgi_app() port = 5001 server = make_server("0.0.0.0", port, app) print(f"Serving on port {port}") server.serve_forever() webargs-8.5.0/examples/requirements.txt000066400000000000000000000001101461250545000202520ustar00rootroot00000000000000python-dateutil==2.9.0.post0 Flask bottle tornado flask-restful pyramid webargs-8.5.0/examples/schema_example.py000066400000000000000000000077431461250545000203360ustar00rootroot00000000000000"""Example implementation of using a marshmallow Schema for both request input and output with a `use_schema` decorator. Run the app: $ python examples/schema_example.py Try the following with httpie (a cURL-like utility, http://httpie.org): $ pip install httpie $ http GET :5001/users/ $ http GET :5001/users/42 $ http POST :5001/users/ username=brian first_name=Brian last_name=May $ http PATCH :5001/users/42 username=freddie $ http GET :5001/users/ limit==1 """ import functools import random from flask import Flask, request from marshmallow import Schema, fields, post_dump from webargs.flaskparser import parser, use_kwargs app = Flask(__name__) ##### Fake database and model ##### class Model: def __init__(self, **kwargs): self.__dict__.update(kwargs) def update(self, **kwargs): self.__dict__.update(kwargs) @classmethod def insert(cls, db, **kwargs): collection = db[cls.collection] new_id = None if "id" in kwargs: # for setting up fixtures new_id = kwargs.pop("id") else: # find a new id found_id = False while not found_id: new_id = random.randint(1, 9999) if new_id not in collection: found_id = True new_record = cls(id=new_id, **kwargs) collection[new_id] = new_record return new_record class User(Model): collection = "users" db = {"users": {}} ##### use_schema ##### def use_schema(schema_cls, list_view=False, locations=None): """View decorator for using a marshmallow schema to (1) parse a request's input and (2) serializing the view's output to a JSON response. """ def decorator(func): @functools.wraps(func) def wrapped(*args, **kwargs): partial = request.method != "POST" schema = schema_cls(partial=partial) use_args_wrapper = parser.use_args(schema, locations=locations) # Function wrapped with use_args func_with_args = use_args_wrapper(func) ret = func_with_args(*args, **kwargs) # support (json, status) tuples if isinstance(ret, tuple) and len(ret) == 2 and isinstance(ret[1], int): return schema.dump(ret[0], many=list_view), ret[1] return schema.dump(ret, many=list_view) return wrapped return decorator ##### Schemas ##### class UserSchema(Schema): id = fields.Int(dump_only=True) username = fields.Str(required=True) first_name = fields.Str() last_name = fields.Str() @post_dump(pass_many=True) def wrap_with_envelope(self, data, many, **kwargs): return {"data": data} ##### Routes ##### @app.route("/users/", methods=["GET", "PATCH"]) @use_schema(UserSchema) def user_detail(reqargs, user_id): user = db["users"].get(user_id) if not user: return {"message": "User not found"}, 404 if request.method == "PATCH" and reqargs: user.update(**reqargs) return user # You can add additional arguments with use_kwargs @app.route("/users/", methods=["GET", "POST"]) @use_kwargs({"limit": fields.Int(load_default=10, location="query")}) @use_schema(UserSchema, list_view=True) def user_list(reqargs, limit): users = db["users"].values() if request.method == "POST": User.insert(db=db, **reqargs) return list(users)[:limit] # Return validation errors as JSON @app.errorhandler(422) @app.errorhandler(400) def handle_validation_error(err): exc = getattr(err, "exc", None) if exc: headers = err.data["headers"] messages = exc.messages else: headers = None messages = ["Invalid request."] if headers: return {"errors": messages}, err.code, headers else: return {"errors": messages}, err.code if __name__ == "__main__": User.insert( db=db, id=42, username="fred", first_name="Freddie", last_name="Mercury" ) app.run(port=5001, debug=True) webargs-8.5.0/examples/tornado_example.py000066400000000000000000000052331461250545000205340ustar00rootroot00000000000000"""A simple number and datetime addition JSON API. Run the app: $ python examples/tornado_example.py Try the following with httpie (a cURL-like utility, http://httpie.org): $ pip install httpie $ http GET :5001/ $ http GET :5001/ name==Ada $ http POST :5001/add x=40 y=2 $ http POST :5001/dateadd value=1973-04-10 addend=63 $ http POST :5001/dateadd value=2014-10-23 addend=525600 unit=minutes """ import datetime as dt import tornado.ioloop from tornado.web import RequestHandler from webargs import fields, validate from webargs.tornadoparser import use_args, use_kwargs class BaseRequestHandler(RequestHandler): def write_error(self, status_code, **kwargs): """Write errors as JSON.""" self.set_header("Content-Type", "application/json") if "exc_info" in kwargs: etype, exc, traceback = kwargs["exc_info"] if hasattr(exc, "messages"): self.write({"errors": exc.messages}) if getattr(exc, "headers", None): for name, val in exc.headers.items(): self.set_header(name, val) self.finish() class HelloHandler(BaseRequestHandler): """A welcome page.""" hello_args = {"name": fields.Str(load_default="Friend")} @use_args(hello_args) def get(self, args): response = {"message": "Welcome, {}!".format(args["name"])} self.write(response) class AdderHandler(BaseRequestHandler): """An addition endpoint.""" add_args = {"x": fields.Float(required=True), "y": fields.Float(required=True)} @use_kwargs(add_args) def post(self, x, y): self.write({"result": x + y}) class DateAddHandler(BaseRequestHandler): """A date adder endpoint.""" dateadd_args = { "value": fields.Date(required=False), "addend": fields.Int(required=True, validate=validate.Range(min=1)), "unit": fields.Str( load_default="days", validate=validate.OneOf(["minutes", "days"]) ), } @use_kwargs(dateadd_args) def post(self, value, addend, unit): """A date adder endpoint.""" value = value or dt.datetime.utcnow() if unit == "minutes": delta = dt.timedelta(minutes=addend) else: delta = dt.timedelta(days=addend) result = value + delta self.write({"result": result.isoformat()}) if __name__ == "__main__": app = tornado.web.Application( [(r"/", HelloHandler), (r"/add", AdderHandler), (r"/dateadd", DateAddHandler)], debug=True, ) port = 5001 app.listen(port) print(f"Serving on port {port}") tornado.ioloop.IOLoop.instance().start() webargs-8.5.0/mypy.ini000066400000000000000000000013511461250545000146570ustar00rootroot00000000000000[mypy] ignore_missing_imports = true warn_unreachable = true warn_unused_ignores = true warn_redundant_casts = true # warn_return_any = true warn_no_return = true no_implicit_optional = true disallow_untyped_defs = true [mypy-webargs.fields] disallow_untyped_defs = false [mypy-webargs.multidictproxy] disallow_untyped_defs = false [mypy-webargs.testing] disallow_untyped_defs = false [mypy-webargs.aiohttpparser] disallow_untyped_defs = false [mypy-webargs.bottleparser] disallow_untyped_defs = false [mypy-webargs.djangoparser] disallow_untyped_defs = false [mypy-webargs.falconparser] disallow_untyped_defs = false [mypy-webargs.pyramidparser] disallow_untyped_defs = false [mypy-webargs.tornadoparser] disallow_untyped_defs = false webargs-8.5.0/pyproject.toml000066400000000000000000000061341461250545000161000ustar00rootroot00000000000000[project] name = "webargs" version = "8.5.0" 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." readme = "README.rst" license = { file = "LICENSE" } authors = [{ name = "Steven Loria", email = "sloria1@gmail.com" }] maintainers = [ { name = "Steven Loria", email = "sloria1@gmail.com" }, { name = "Jérôme Lafréchoux", email = "jerome@jolimont.fr" }, { name = "Stephen Rosen", email = "sirosen0@gmail.com" }, ] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", ] keywords = [ "webargs", "http", "flask", "django", "bottle", "tornado", "aiohttp", "request", "arguments", "validation", "parameters", "rest", "api", "marshmallow", ] requires-python = ">=3.8" dependencies = ["marshmallow>=3.0.0", "packaging>=17.0"] [project.urls] Changelog = "https://webargs.readthedocs.io/en/latest/changelog.html" Funding = "https://opencollective.com/marshmallow" Issues = "https://github.com/marshmallow-code/webargs/issues" Source = "https://github.com/marshmallow-code/webargs" Tidelift = "https://tidelift.com/subscription/pkg/pypi-webargs?utm_source=pypi-webargs&utm_medium=pypi" [project.optional-dependencies] frameworks = [ "Flask>=0.12.5", "Django>=2.2.0", "bottle>=0.12.13", "tornado>=4.5.2", "pyramid>=1.9.1", "falcon>=2.0.0", "aiohttp>=3.0.8", ] tests = [ "webargs[frameworks]", "pytest", "pytest-asyncio", "webtest==3.0.0", "webtest-aiohttp==2.0.0", "pytest-aiohttp>=0.3.0", ] docs = [ "webargs[frameworks]", "Sphinx==7.3.7", "sphinx-issues==4.1.0", "furo==2024.1.29", ] dev = ["webargs[tests]", "tox", "pre-commit~=3.5"] [build-system] requires = ["flit_core<4"] build-backend = "flit_core.buildapi" [tool.flit.sdist] include = [ "docs/", "tests/", "CHANGELOG.rst", "CONTRIBUTING.rst", "SECURITY.md", "NOTICE", "tox.ini", ] exclude = ["docs/_build/"] [tool.ruff] src = ["src"] fix = true show-fixes = true show-source = true [tool.ruff.lint] ignore = ["E203", "E266", "E501", "E731"] select = [ "B", # flake8-bugbear "E", # pycodestyle error "F", # pyflakes "I", # isort "UP", # pyupgrade "W", # pycodestyle warning ] [tool.pytest.ini_options] filterwarnings = [ # https://github.com/Pylons/pyramid/issues/3731 "ignore:.*pkg_resources.*:DeprecationWarning", # https://github.com/Pylons/webob/issues/437 "ignore:.*'cgi' is deprecated.*:DeprecationWarning", # https://github.com/sloria/webtest-aiohttp/issues/6 "ignore:.*The object should be created within an async function.*:DeprecationWarning", ] webargs-8.5.0/src/000077500000000000000000000000001461250545000137475ustar00rootroot00000000000000webargs-8.5.0/src/webargs/000077500000000000000000000000001461250545000154015ustar00rootroot00000000000000webargs-8.5.0/src/webargs/__init__.py000077500000000000000000000013451461250545000175200ustar00rootroot00000000000000from __future__ import annotations import importlib.metadata # Make marshmallow's validation functions importable from webargs from marshmallow import validate from marshmallow.utils import missing from packaging.version import Version from webargs import fields from webargs.core import ValidationError # TODO: Deprecate __version__ et al. __version__ = importlib.metadata.version("webargs") __parsed_version__ = Version(__version__) __version_info__: tuple[int, int, int] | tuple[int, int, int, str, int] = ( __parsed_version__.release ) # type: ignore[assignment] if __parsed_version__.pre: __version_info__ += __parsed_version__.pre # type: ignore[assignment] __all__ = ("ValidationError", "fields", "missing", "validate") webargs-8.5.0/src/webargs/aiohttpparser.py000066400000000000000000000136271461250545000206510ustar00rootroot00000000000000"""aiohttp request argument parsing module. Example: :: import asyncio from aiohttp import web from webargs import fields from webargs.aiohttpparser import use_args hello_args = { 'name': fields.Str(required=True) } @asyncio.coroutine @use_args(hello_args) def index(request, args): return web.Response( body='Hello {}'.format(args['name']).encode('utf-8') ) app = web.Application() app.router.add_route('GET', '/', index) """ from __future__ import annotations import typing from aiohttp import web, web_exceptions from marshmallow import RAISE, Schema, ValidationError from webargs import core from webargs.asyncparser import AsyncParser from webargs.core import json from webargs.multidictproxy import MultiDictProxy def is_json_request(req) -> bool: content_type = req.content_type return core.is_json(content_type) class HTTPUnprocessableEntity(web.HTTPClientError): status_code = 422 # Mapping of status codes to exception classes # Adapted from werkzeug exception_map: dict[int, type[web_exceptions.HTTPException]] = {} exception_map[422] = HTTPUnprocessableEntity def _find_exceptions() -> None: for name in web_exceptions.__all__: obj = getattr(web_exceptions, name) try: is_http_exception = issubclass(obj, web_exceptions.HTTPException) except TypeError: is_http_exception = False if not is_http_exception or obj.status_code is None: continue old_obj = exception_map.get(obj.status_code, None) if old_obj is not None and issubclass(obj, old_obj): continue exception_map[obj.status_code] = obj # Collect all exceptions from aiohttp.web_exceptions _find_exceptions() del _find_exceptions class AIOHTTPParser(AsyncParser[web.Request]): """aiohttp request argument parser.""" DEFAULT_UNKNOWN_BY_LOCATION: dict[str, str | None] = { "match_info": RAISE, "path": RAISE, **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION, } __location_map__ = dict( match_info="load_match_info", path="load_match_info", **core.Parser.__location_map__, ) def load_querystring(self, req, schema: Schema) -> MultiDictProxy: """Return query params from the request as a MultiDictProxy.""" return self._makeproxy(req.query, schema) async def load_form(self, req, schema: Schema) -> MultiDictProxy: """Return form values from the request as a MultiDictProxy.""" post_data = await req.post() return self._makeproxy(post_data, schema) async def load_json_or_form(self, req, schema: Schema) -> dict | MultiDictProxy: data = await self.load_json(req, schema) if data is not core.missing: return data return await self.load_form(req, schema) async def load_json(self, req, schema: Schema): """Return a parsed json payload from the request.""" if not (req.body_exists and is_json_request(req)): return core.missing try: return await req.json(loads=json.loads) except json.JSONDecodeError as exc: if exc.doc == "": return core.missing return self._handle_invalid_json_error(exc, req) except UnicodeDecodeError as exc: return self._handle_invalid_json_error(exc, req) def load_headers(self, req, schema: Schema) -> MultiDictProxy: """Return headers from the request as a MultiDictProxy.""" return self._makeproxy(req.headers, schema) def load_cookies(self, req, schema: Schema) -> MultiDictProxy: """Return cookies from the request as a MultiDictProxy.""" return self._makeproxy(req.cookies, schema) def load_files(self, req, schema: Schema) -> typing.NoReturn: raise NotImplementedError( "load_files is not implemented. You may be able to use load_form for " "parsing upload data." ) def load_match_info(self, req, schema: Schema) -> typing.Mapping: """Load the request's ``match_info``.""" return req.match_info def get_request_from_view_args( self, view: typing.Callable, args: typing.Iterable, kwargs: typing.Mapping ): """Get request object from a handler function or method. Used internally by ``use_args`` and ``use_kwargs``. """ req = None for arg in args: if isinstance(arg, web.Request): req = arg break if isinstance(arg, web.View): req = arg.request break if not isinstance(req, web.Request): raise ValueError("Request argument not found for handler") return req def handle_error( self, error: ValidationError, req, schema: Schema, *, error_status_code: int | None, error_headers: typing.Mapping[str, str] | None, ) -> typing.NoReturn: """Handle ValidationErrors and return a JSON response of error messages to the client. """ error_class = exception_map.get( error_status_code or self.DEFAULT_VALIDATION_STATUS ) if not error_class: raise LookupError(f"No exception for {error_status_code}") headers = error_headers raise error_class( text=json.dumps(error.messages), headers=headers, content_type="application/json", ) def _handle_invalid_json_error( self, error: json.JSONDecodeError | UnicodeDecodeError, req, *args, **kwargs ) -> typing.NoReturn: error_class = exception_map[400] messages = {"json": ["Invalid JSON body."]} raise error_class(text=json.dumps(messages), content_type="application/json") parser = AIOHTTPParser() use_args = parser.use_args # type: typing.Callable use_kwargs = parser.use_kwargs # type: typing.Callable webargs-8.5.0/src/webargs/asyncparser.py000066400000000000000000000020771461250545000203130ustar00rootroot00000000000000"""Asynchronous request parser.""" from __future__ import annotations import typing from webargs import core class AsyncParser(core.Parser[core.Request]): """Asynchronous variant of `webargs.core.Parser`. The ``parse`` method is redefined to be ``async``. """ async def parse( self, argmap: core.ArgMap, req: core.Request | None = None, *, location: str | None = None, unknown: str | None = core._UNKNOWN_DEFAULT_PARAM, validate: core.ValidateArg = None, error_status_code: int | None = None, error_headers: typing.Mapping[str, str] | None = None, ) -> typing.Any: """Coroutine variant of `webargs.core.Parser`. Receives the same arguments as `webargs.core.Parser.parse`. """ data = await self.async_parse( argmap, req, location=location, unknown=unknown, validate=validate, error_status_code=error_status_code, error_headers=error_headers, ) return data webargs-8.5.0/src/webargs/bottleparser.py000066400000000000000000000060011461250545000204560ustar00rootroot00000000000000"""Bottle request argument parsing module. Example: :: from bottle import route, run from marshmallow import fields from webargs.bottleparser import use_args hello_args = { 'name': fields.Str(load_default='World') } @route('/', method='GET', apply=use_args(hello_args)) def index(args): return 'Hello ' + args['name'] if __name__ == '__main__': run(debug=True) """ import bottle from webargs import core class BottleParser(core.Parser[bottle.Request]): """Bottle.py request argument parser.""" def _handle_invalid_json_error(self, error, req, *args, **kwargs): raise bottle.HTTPError( status=400, body={"json": ["Invalid JSON body."]}, exception=error ) def _raw_load_json(self, req): """Read a json payload from the request.""" try: data = req.json except AttributeError: return core.missing # unfortunately, bottle does not distinguish between an empty body, "", # and a body containing the valid JSON value null, "null" # so these can't be properly disambiguated # as our best-effort solution, treat None as missing and ignore the # (admittedly unusual) "null" case # see: https://github.com/bottlepy/bottle/issues/1160 if data is None: return core.missing return data def load_querystring(self, req, schema): """Return query params from the request as a MultiDictProxy.""" return self._makeproxy(req.query, schema) def load_form(self, req, schema): """Return form values from the request as a MultiDictProxy.""" # For consistency with other parsers' behavior, don't attempt to # parse if content-type is mismatched. # TODO: Make this check more specific if core.is_json(req.content_type): return core.missing return self._makeproxy(req.forms, schema) def load_headers(self, req, schema): """Return headers from the request as a MultiDictProxy.""" return self._makeproxy(req.headers, schema) def load_cookies(self, req, schema): """Return cookies from the request.""" return req.cookies def load_files(self, req, schema): """Return files from the request as a MultiDictProxy.""" return self._makeproxy(req.files, schema) def handle_error(self, error, req, schema, *, error_status_code, error_headers): """Handles errors during parsing. Aborts the current request with a 400 error. """ status_code = error_status_code or self.DEFAULT_VALIDATION_STATUS raise bottle.HTTPError( status=status_code, body=error.messages, headers=error_headers, exception=error, ) def get_default_request(self): """Override to use bottle's thread-local request object by default.""" return bottle.request parser = BottleParser() use_args = parser.use_args use_kwargs = parser.use_kwargs webargs-8.5.0/src/webargs/core.py000066400000000000000000001005731461250545000167110ustar00rootroot00000000000000from __future__ import annotations import asyncio import functools import inspect import json import logging import typing import marshmallow as ma from marshmallow import ValidationError from marshmallow.utils import missing from webargs.multidictproxy import MultiDictProxy logger = logging.getLogger(__name__) __all__ = [ "ValidationError", "Parser", "missing", "parse_json", ] Request = typing.TypeVar("Request") ArgMap = typing.Union[ ma.Schema, typing.Type[ma.Schema], typing.Mapping[str, typing.Union[ma.fields.Field, typing.Type[ma.fields.Field]]], typing.Callable[[Request], ma.Schema], ] ValidateArg = typing.Union[None, typing.Callable, typing.Iterable[typing.Callable]] CallableList = typing.List[typing.Callable] ErrorHandler = typing.Callable[..., typing.NoReturn] # generic type var with no particular meaning T = typing.TypeVar("T") # type var for callables, to make type-preserving decorators C = typing.TypeVar("C", bound=typing.Callable) # type var for multidict proxy classes MultiDictProxyT = typing.TypeVar("MultiDictProxyT", bound=MultiDictProxy) # type var for a callable which is an error handler # used to ensure that the error_handler decorator is type preserving ErrorHandlerT = typing.TypeVar("ErrorHandlerT", bound=ErrorHandler) AsyncErrorHandler = typing.Callable[..., typing.Awaitable[typing.NoReturn]] # a value used as the default for arguments, so that when `None` is passed, it # can be distinguished from the default value _UNKNOWN_DEFAULT_PARAM = "_default" DEFAULT_VALIDATION_STATUS: int = 422 def _record_arg_name(f: typing.Callable[..., typing.Any], argname: str | None) -> None: if argname is None: return if not hasattr(f, "__webargs_argnames__"): f.__webargs_argnames__ = () # type: ignore[attr-defined] f.__webargs_argnames__ += (argname,) # type: ignore[attr-defined] def _iscallable(x: typing.Any) -> bool: # workaround for # https://github.com/python/mypy/issues/9778 return callable(x) def _callable_or_raise(obj: T | None) -> T | None: """Makes sure an object is callable if it is not ``None``. If not callable, a ValueError is raised. """ if obj and not _iscallable(obj): raise ValueError(f"{obj!r} is not callable.") return obj def get_mimetype(content_type: str) -> str: return content_type.split(";")[0].strip() # Adapted from werkzeug: # https://github.com/mitsuhiko/werkzeug/blob/master/werkzeug/wrappers.py def is_json(mimetype: str | None) -> bool: """Indicates if this mimetype is JSON or not. By default a request is considered to include JSON data if the mimetype is ``application/json`` or ``application/*+json``. """ if not mimetype: return False if ";" in mimetype: # Allow Content-Type header to be passed mimetype = get_mimetype(mimetype) if mimetype == "application/json": return True if mimetype.startswith("application/") and mimetype.endswith("+json"): return True return False def parse_json(s: typing.AnyStr, *, encoding: str = "utf-8") -> typing.Any: if isinstance(s, str): decoded = s else: try: decoded = s.decode(encoding) except UnicodeDecodeError as exc: raise json.JSONDecodeError( f"Bytes decoding error : {exc.reason}", doc=str(exc.object), pos=exc.start, ) from exc return json.loads(decoded) def _ensure_list_of_callables(obj: typing.Any) -> CallableList: if obj: if isinstance(obj, (list, tuple)): validators = typing.cast(CallableList, list(obj)) elif callable(obj): validators = [obj] else: raise ValueError(f"{obj!r} is not a callable or list of callables.") else: validators = [] return validators class Parser(typing.Generic[Request]): """Base parser class that provides high-level implementation for parsing a request. Descendant classes must provide lower-level implementations for reading data from different locations, e.g. ``load_json``, ``load_querystring``, etc. :param str location: Default location to use for data :param str unknown: A default value to pass for ``unknown`` when calling the schema's ``load`` method. Defaults to ``marshmallow.EXCLUDE`` for non-body locations and ``marshmallow.RAISE`` for request bodies. Pass ``None`` to use the schema's setting instead. :param callable error_handler: Custom error handler function. """ #: Default location to check for data DEFAULT_LOCATION: str = "json" #: Default value to use for 'unknown' on schema load # on a per-location basis DEFAULT_UNKNOWN_BY_LOCATION: dict[str, str | None] = { "json": None, "form": None, "json_or_form": None, "querystring": ma.EXCLUDE, "query": ma.EXCLUDE, "headers": ma.EXCLUDE, "cookies": ma.EXCLUDE, "files": ma.EXCLUDE, } #: The marshmallow Schema class to use when creating new schemas DEFAULT_SCHEMA_CLASS: type[ma.Schema] = ma.Schema #: Default status code to return for validation errors DEFAULT_VALIDATION_STATUS: int = DEFAULT_VALIDATION_STATUS #: Default error message for validation errors DEFAULT_VALIDATION_MESSAGE: str = "Invalid value." #: field types which should always be treated as if they set `is_multiple=True` KNOWN_MULTI_FIELDS: list[type] = [ma.fields.List, ma.fields.Tuple] #: set use_args to use a positional argument (rather than a keyword argument) # defaults to True, but will become False in a future major version USE_ARGS_POSITIONAL: bool = True #: Maps location => method name __location_map__: dict[str, str | typing.Callable] = { "json": "load_json", "querystring": "load_querystring", "query": "load_querystring", "form": "load_form", "headers": "load_headers", "cookies": "load_cookies", "files": "load_files", "json_or_form": "load_json_or_form", } def __init__( self, location: str | None = None, *, unknown: str | None = _UNKNOWN_DEFAULT_PARAM, error_handler: ErrorHandler | None = None, schema_class: type[ma.Schema] | None = None, ) -> None: self.location = location or self.DEFAULT_LOCATION self.error_callback: ErrorHandler | None = _callable_or_raise(error_handler) self.schema_class = schema_class or self.DEFAULT_SCHEMA_CLASS self.unknown = unknown def _makeproxy( self, multidict: typing.Any, schema: ma.Schema, *, cls: type[MultiDictProxyT] | type[MultiDictProxy] = MultiDictProxy, ) -> MultiDictProxyT | MultiDictProxy: """Create a multidict proxy object with options from the current parser""" return cls(multidict, schema, known_multi_fields=tuple(self.KNOWN_MULTI_FIELDS)) def _get_loader(self, location: str) -> typing.Callable: """Get the loader function for the given location. :raises: ValueError if a given location is invalid. """ valid_locations = set(self.__location_map__.keys()) if location not in valid_locations: raise ValueError(f"Invalid location argument: {location}") # Parsing function to call # May be a method name (str) or a function func = self.__location_map__[location] if isinstance(func, str): return getattr(self, func) return func def _load_location_data( self, *, schema: ma.Schema, req: Request, location: str ) -> typing.Any: """Return a dictionary-like object for the location on the given request. Needs to have the schema in hand in order to correctly handle loading lists from multidict objects and `many=True` schemas. """ loader_func = self._get_loader(location) return loader_func(req, schema) async def _async_load_location_data( self, schema: ma.Schema, req: Request, location: str ) -> typing.Any: # an async variant of the _load_location_data method # the loader function itself may or may not be async loader_func = self._get_loader(location) data = loader_func(req, schema) if inspect.isawaitable(data): return await data return data def _on_validation_error( self, error: ValidationError, req: Request, schema: ma.Schema, location: str, *, error_status_code: int | None, error_headers: typing.Mapping[str, str] | None, ) -> typing.NoReturn: # rewrite messages to be namespaced under the location which created # them # e.g. {"json":{"foo":["Not a valid integer."]}} # instead of # {"foo":["Not a valid integer."]} error.messages = {location: error.messages} error_handler: ErrorHandler = self.error_callback or self.handle_error error_handler( error, req, schema, error_status_code=error_status_code, error_headers=error_headers, ) async def _async_on_validation_error( self, error: ValidationError, req: Request, schema: ma.Schema, location: str, *, error_status_code: int | None, error_headers: typing.Mapping[str, str] | None, ) -> typing.NoReturn: # an async-aware variant of the _on_validation_error method error.messages = {location: error.messages} error_handler = self.error_callback or self.handle_error # an async error handler was registered, await it if asyncio.iscoroutinefunction(error_handler): async_error_handler = typing.cast(AsyncErrorHandler, error_handler) await async_error_handler( error, req, schema, error_status_code=error_status_code, error_headers=error_headers, ) # the error handler was synchronous (e.g. Parser.handle_error) so it # will raise an error else: error_handler( error, req, schema, error_status_code=error_status_code, error_headers=error_headers, ) def _validate_arguments(self, data: typing.Any, validators: CallableList) -> None: # although `data` is typically a Mapping, nothing forbids a `schema.load` # from returning an arbitrary object subject to validators for validator in validators: if validator(data) is False: msg = self.DEFAULT_VALIDATION_MESSAGE raise ValidationError(msg, data=data) def _get_schema(self, argmap: ArgMap, req: Request) -> ma.Schema: """Return a `marshmallow.Schema` for the given argmap and request. :param argmap: Either a `marshmallow.Schema`, `dict` of argname -> `marshmallow.fields.Field` pairs, or a callable that returns a `marshmallow.Schema` instance. :param req: The request object being parsed. :rtype: marshmallow.Schema """ if isinstance(argmap, ma.Schema): schema = argmap elif isinstance(argmap, type) and issubclass(argmap, ma.Schema): schema = argmap() elif callable(argmap): schema = argmap(req) elif isinstance(argmap, typing.Mapping): if not isinstance(argmap, dict): argmap = dict(argmap) schema = self.schema_class.from_dict(argmap)() else: raise TypeError(f"argmap was of unexpected type {type(argmap)}") return schema def _prepare_for_parse( self, argmap: ArgMap, req: Request | None = None, location: str | None = None, unknown: str | None = _UNKNOWN_DEFAULT_PARAM, validate: ValidateArg = None, ) -> tuple[None, Request, str, CallableList, ma.Schema]: # validate parse() arguments and handle defaults # (shared between sync and async variants) req = req if req is not None else self.get_default_request() if req is None: raise ValueError("Must pass req object") location = location or self.location validators = _ensure_list_of_callables(validate) schema = self._get_schema(argmap, req) return (None, req, location, validators, schema) def _process_location_data( self, location_data: typing.Any, schema: ma.Schema, req: Request, location: str, unknown: str | None, validators: CallableList, ) -> typing.Any: # after the data has been fetched from a registered location, # this is how it is processed # (shared between sync and async variants) # when the desired location is empty (no data), provide an empty # dict as the default so that optional arguments in a location # (e.g. optional JSON body) work smoothly if location_data is missing: location_data = {} # precedence order: explicit, instance setting, default per location unknown = ( unknown if unknown != _UNKNOWN_DEFAULT_PARAM else ( self.unknown if self.unknown != _UNKNOWN_DEFAULT_PARAM else self.DEFAULT_UNKNOWN_BY_LOCATION.get(location) ) ) load_kwargs: dict[str, typing.Any] = {"unknown": unknown} if unknown else {} preprocessed_data = self.pre_load( location_data, schema=schema, req=req, location=location ) data = schema.load(preprocessed_data, **load_kwargs) self._validate_arguments(data, validators) return data def parse( self, argmap: ArgMap, req: Request | None = None, *, location: str | None = None, unknown: str | None = _UNKNOWN_DEFAULT_PARAM, validate: ValidateArg = None, error_status_code: int | None = None, error_headers: typing.Mapping[str, str] | None = None, ) -> typing.Any: """Main request parsing method. :param argmap: Either a `marshmallow.Schema`, a `dict` of argname -> `marshmallow.fields.Field` pairs, or a callable which accepts a request and returns a `marshmallow.Schema`. :param req: The request object to parse. :param str location: Where on the request to load values. Can be any of the values in :py:attr:`~__location_map__`. By default, that means one of ``('json', 'query', 'querystring', 'form', 'headers', 'cookies', 'files', 'json_or_form')``. :param str unknown: A value to pass for ``unknown`` when calling the schema's ``load`` method. Defaults to ``marshmallow.EXCLUDE`` for non-body locations and ``marshmallow.RAISE`` for request bodies. Pass ``None`` to use the schema's setting instead. :param callable validate: Validation function or list of validation functions that receives the dictionary of parsed arguments. Validator either returns a boolean or raises a :exc:`ValidationError`. :param int error_status_code: Status code passed to error handler functions when a `ValidationError` is raised. :param dict error_headers: Headers passed to error handler functions when a a `ValidationError` is raised. :return: A dictionary of parsed arguments """ data, req, location, validators, schema = self._prepare_for_parse( argmap, req, location, unknown, validate ) try: location_data = self._load_location_data( schema=schema, req=req, location=location ) data = self._process_location_data( location_data, schema, req, location, unknown, validators ) except ma.exceptions.ValidationError as error: self._on_validation_error( error, req, schema, location, error_status_code=error_status_code, error_headers=error_headers, ) raise ValueError( "_on_validation_error hook did not raise an exception" ) from error return data async def async_parse( self, argmap: ArgMap, req: Request | None = None, *, location: str | None = None, unknown: str | None = _UNKNOWN_DEFAULT_PARAM, validate: ValidateArg = None, error_status_code: int | None = None, error_headers: typing.Mapping[str, str] | None = None, ) -> typing.Any: """Coroutine variant of `webargs.core.Parser.parse`. Receives the same arguments as `webargs.core.Parser.parse`. """ data, req, location, validators, schema = self._prepare_for_parse( argmap, req, location, unknown, validate ) try: location_data = await self._async_load_location_data( schema=schema, req=req, location=location ) data = self._process_location_data( location_data, schema, req, location, unknown, validators ) except ma.exceptions.ValidationError as error: await self._async_on_validation_error( error, req, schema, location, error_status_code=error_status_code, error_headers=error_headers, ) raise ValueError( "_on_validation_error hook did not raise an exception" ) from error return data def get_default_request(self) -> Request | None: """Optional override. Provides a hook for frameworks that use thread-local request objects. """ return None def get_request_from_view_args( self, view: typing.Callable, args: tuple, kwargs: typing.Mapping[str, typing.Any], ) -> Request | None: """Optional override. Returns the request object to be parsed, given a view function's args and kwargs. Used by the `use_args` and `use_kwargs` to get a request object from a view's arguments. :param callable view: The view function or method being decorated by `use_args` or `use_kwargs` :param tuple args: Positional arguments passed to ``view``. :param dict kwargs: Keyword arguments passed to ``view``. """ return None @staticmethod def _update_args_kwargs( args: tuple, kwargs: dict[str, typing.Any], parsed_args: dict[str, typing.Any], as_kwargs: bool, arg_name: str | None, ) -> tuple[tuple, dict[str, typing.Any]]: """Update args or kwargs with parsed_args depending on as_kwargs""" if as_kwargs: # expand parsed_args into kwargs kwargs.update(parsed_args) else: if arg_name: # add parsed_args as a specific kwarg kwargs[arg_name] = parsed_args else: # Add parsed_args after other positional arguments args += (parsed_args,) return args, kwargs def use_args( self, argmap: ArgMap, req: Request | None = None, *, location: str | None = None, unknown: str | None = _UNKNOWN_DEFAULT_PARAM, as_kwargs: bool = False, arg_name: str | None = None, validate: ValidateArg = None, error_status_code: int | None = None, error_headers: typing.Mapping[str, str] | None = None, ) -> typing.Callable[..., typing.Callable]: """Decorator that injects parsed arguments into a view function or method. Example usage with Flask: :: @app.route('/echo', methods=['get', 'post']) @parser.use_args({'name': fields.Str()}, location="querystring") def greet(querystring_args): return 'Hello ' + querystring_args['name'] :param argmap: Either a `marshmallow.Schema`, a `dict` of argname -> `marshmallow.fields.Field` pairs, or a callable which accepts a request and returns a `marshmallow.Schema`. :param str location: Where on the request to load values. :param str unknown: A value to pass for ``unknown`` when calling the schema's ``load`` method. :param bool as_kwargs: Whether to insert arguments as keyword arguments. :param str arg_name: Keyword argument name to use for arguments. Mutually exclusive with as_kwargs. :param callable validate: Validation function that receives the dictionary of parsed arguments. If the function returns ``False``, the parser will raise a :exc:`ValidationError`. :param int error_status_code: Status code passed to error handler functions when a `ValidationError` is raised. :param dict error_headers: Headers passed to error handler functions when a a `ValidationError` is raised. """ location = location or self.location request_obj = req # Optimization: If argmap is passed as a dictionary, we only need # to generate a Schema once if isinstance(argmap, typing.Mapping): if not isinstance(argmap, dict): argmap = dict(argmap) argmap = self.schema_class.from_dict(argmap)() if arg_name is not None and as_kwargs: raise ValueError("arg_name and as_kwargs are mutually exclusive") if arg_name is None and not self.USE_ARGS_POSITIONAL: arg_name = self.get_default_arg_name(location, argmap) def decorator(func: typing.Callable) -> typing.Callable: req_ = request_obj # check at decoration time that a unique name is being used # (no arg_name conflicts) if arg_name is not None and not as_kwargs: existing_arg_names = getattr(func, "__webargs_argnames__", ()) if arg_name in existing_arg_names: raise ValueError( f"Attempted to pass `arg_name='{arg_name}'` via use_args() but " "that name was already used. If this came from stacked webargs " "decorators, try setting `arg_name` to distinguish usages." ) if asyncio.iscoroutinefunction(func): @functools.wraps(func) async def wrapper( *args: typing.Any, **kwargs: typing.Any ) -> typing.Any: req_obj = req_ if not req_obj: req_obj = self.get_request_from_view_args(func, args, kwargs) # NOTE: At this point, argmap may be a Schema, callable, or dict parsed_args = await self.async_parse( argmap, req=req_obj, location=location, unknown=unknown, validate=validate, error_status_code=error_status_code, error_headers=error_headers, ) args, kwargs = self._update_args_kwargs( args, kwargs, parsed_args, as_kwargs, arg_name ) return await func(*args, **kwargs) else: @functools.wraps(func) def wrapper(*args: typing.Any, **kwargs: typing.Any) -> typing.Any: req_obj = req_ if not req_obj: req_obj = self.get_request_from_view_args(func, args, kwargs) # NOTE: At this point, argmap may be a Schema, callable, or dict parsed_args = self.parse( argmap, req=req_obj, location=location, unknown=unknown, validate=validate, error_status_code=error_status_code, error_headers=error_headers, ) args, kwargs = self._update_args_kwargs( args, kwargs, parsed_args, as_kwargs, arg_name ) return func(*args, **kwargs) wrapper.__wrapped__ = func # type: ignore _record_arg_name(wrapper, arg_name) return wrapper return decorator def use_kwargs( self, argmap: ArgMap, req: Request | None = None, *, location: str | None = None, unknown: str | None = _UNKNOWN_DEFAULT_PARAM, validate: ValidateArg = None, error_status_code: int | None = None, error_headers: typing.Mapping[str, str] | None = None, ) -> typing.Callable[..., typing.Callable]: """Decorator that injects parsed arguments into a view function or method as keyword arguments. This is a shortcut to :meth:`use_args` with ``as_kwargs=True``. Example usage with Flask: :: @app.route('/echo', methods=['get', 'post']) @parser.use_kwargs({'name': fields.Str()}) def greet(name): return 'Hello ' + name Receives the same ``args`` and ``kwargs`` as :meth:`use_args`. """ return self.use_args( argmap, req=req, as_kwargs=True, location=location, unknown=unknown, validate=validate, error_status_code=error_status_code, error_headers=error_headers, ) def get_default_arg_name(self, location: str, schema: ArgMap) -> str: """This method provides the rule by which an argument name is derived for :meth:`use_args` if no explicit ``arg_name`` is provided. By default, the format used is ``{location}_args``. Users may override this method to customize the default argument naming scheme. ``schema`` will be the argument map or schema passed to :meth:`use_args` unless a dict was used, in which case it will be the schema derived from that dict. """ return f"{location}_args" def location_loader(self, name: str) -> typing.Callable[[C], C]: """Decorator that registers a function for loading a request location. The wrapped function receives a schema and a request. The schema will usually not be relevant, but it's important in some cases -- most notably in order to correctly load multidict values into list fields. Without the schema, there would be no way to know whether to simply `.get()` or `.getall()` from a multidict for a given value. Example: :: from webargs import core parser = core.Parser() @parser.location_loader("name") def load_data(request, schema): return request.data :param str name: The name of the location to register. """ def decorator(func: C) -> C: self.__location_map__[name] = func return func return decorator def error_handler(self, func: ErrorHandlerT) -> ErrorHandlerT: """Decorator that registers a custom error handling function. The function should receive the raised error, request object, `marshmallow.Schema` instance used to parse the request, error status code, and headers to use for the error response. Overrides the parser's ``handle_error`` method. Example: :: from webargs import flaskparser parser = flaskparser.FlaskParser() class CustomError(Exception): pass @parser.error_handler def handle_error(error, req, schema, *, error_status_code, error_headers): raise CustomError(error.messages) :param callable func: The error callback to register. """ self.error_callback = func return func def pre_load( self, location_data: typing.Mapping, *, schema: ma.Schema, req: Request, location: str, ) -> typing.Mapping: """A method of the parser which can transform data after location loading is done. By default it does nothing, but users can subclass parsers and override this method. """ return location_data def _handle_invalid_json_error( self, error: json.JSONDecodeError | UnicodeDecodeError, req: Request, *args: typing.Any, **kwargs: typing.Any, ) -> typing.NoReturn: """Internal hook for overriding treatment of JSONDecodeErrors. Invoked by default `load_json` implementation. External parsers can just implement their own behavior for load_json , so this is not part of the public parser API. """ raise error def load_json(self, req: Request, schema: ma.Schema) -> typing.Any: """Load JSON from a request object or return `missing` if no value can be found. """ # NOTE: although this implementation is real/concrete and used by # several of the parsers in webargs, it relies on the internal hooks # `_handle_invalid_json_error` and `_raw_load_json` # these methods are not part of the public API and are used to simplify # code sharing amongst the built-in webargs parsers try: return self._raw_load_json(req) except json.JSONDecodeError as exc: if exc.doc == "": return missing return self._handle_invalid_json_error(exc, req) except UnicodeDecodeError as exc: return self._handle_invalid_json_error(exc, req) def load_json_or_form(self, req: Request, schema: ma.Schema) -> typing.Any: """Load data from a request, accepting either JSON or form-encoded data. The data will first be loaded as JSON, and, if that fails, it will be loaded as a form post. """ data = self.load_json(req, schema) if data is not missing: return data return self.load_form(req, schema) # Abstract Methods def _raw_load_json(self, req: Request) -> typing.Any: """Internal hook method for implementing load_json() Get a request body for feeding in to `load_json`, and parse it either using core.parse_json() or similar utilities which raise JSONDecodeErrors. Ensure consistent behavior when encountering decoding errors. The default implementation here simply returns `missing`, and the default implementation of `load_json` above will pass that value through. However, by implementing a "mostly concrete" version of load_json with this as a hook for getting data, we consolidate the logic for handling those JSONDecodeErrors. """ return missing def load_querystring(self, req: Request, schema: ma.Schema) -> typing.Any: """Load the query string of a request object or return `missing` if no value can be found. """ return missing def load_form(self, req: Request, schema: ma.Schema) -> typing.Any: """Load the form data of a request object or return `missing` if no value can be found. """ return missing def load_headers(self, req: Request, schema: ma.Schema) -> typing.Any: """Load the headers or return `missing` if no value can be found.""" return missing def load_cookies(self, req: Request, schema: ma.Schema) -> typing.Any: """Load the cookies from the request or return `missing` if no value can be found. """ return missing def load_files(self, req: Request, schema: ma.Schema) -> typing.Any: """Load files from the request or return `missing` if no values can be found. """ return missing def handle_error( self, error: ValidationError, req: Request, schema: ma.Schema, *, error_status_code: int | None, error_headers: typing.Mapping[str, str] | None, ) -> typing.NoReturn: """Called if an error occurs while parsing args. By default, just logs and raises ``error``. """ logger.error(error) raise error webargs-8.5.0/src/webargs/djangoparser.py000066400000000000000000000052131461250545000204330ustar00rootroot00000000000000"""Django request argument parsing. Example usage: :: from django.views.generic import View from django.http import HttpResponse from marshmallow import fields from webargs.djangoparser import use_args hello_args = { 'name': fields.Str(load_default='World') } class MyView(View): @use_args(hello_args) def get(self, args, request): return HttpResponse('Hello ' + args['name']) """ import django import django.http from webargs import core def is_json_request(req): return core.is_json(req.content_type) class DjangoParser(core.Parser[django.http.HttpRequest]): """Django request argument parser. .. warning:: :class:`DjangoParser` does not override :meth:`handle_error `, so your Django views are responsible for catching any :exc:`ValidationErrors` raised by the parser and returning the appropriate `HTTPResponse`. """ def _raw_load_json(self, req: django.http.HttpRequest): """Read a json payload from the request for the core parser's load_json Checks the input mimetype and may return 'missing' if the mimetype is non-json, even if the request body is parseable as json.""" if not is_json_request(req): return core.missing return core.parse_json(req.body) def load_querystring(self, req: django.http.HttpRequest, schema): """Return query params from the request as a MultiDictProxy.""" return self._makeproxy(req.GET, schema) def load_form(self, req: django.http.HttpRequest, schema): """Return form values from the request as a MultiDictProxy.""" return self._makeproxy(req.POST, schema) def load_cookies(self, req: django.http.HttpRequest, schema): """Return cookies from the request.""" return req.COOKIES def load_headers(self, req: django.http.HttpRequest, schema): """Return headers from the request.""" # Django's HttpRequest.headers is a case-insensitive dict type, but it # isn't a multidict, so this is not proxied return req.headers def load_files(self, req: django.http.HttpRequest, schema): """Return files from the request as a MultiDictProxy.""" return self._makeproxy(req.FILES, schema) def get_request_from_view_args(self, view, args, kwargs): # The first argument is either `self` or `request` try: # self.request return args[0].request except AttributeError: # first arg is request return args[0] parser = DjangoParser() use_args = parser.use_args use_kwargs = parser.use_kwargs webargs-8.5.0/src/webargs/falconparser.py000066400000000000000000000152051461250545000204350ustar00rootroot00000000000000"""Falcon request argument parsing module.""" import falcon import marshmallow as ma from falcon.util.uri import parse_query_string from webargs import core HTTP_422 = "422 Unprocessable Entity" # Mapping of int status codes to string status status_map = {422: HTTP_422} # Collect all exceptions from falcon.status_codes def _find_exceptions(): for name in filter(lambda n: n.startswith("HTTP"), dir(falcon.status_codes)): status = getattr(falcon.status_codes, name) status_code = int(status.split(" ")[0]) status_map[status_code] = status _find_exceptions() del _find_exceptions def is_json_request(req: falcon.Request): content_type = req.get_header("Content-Type") return content_type and core.is_json(content_type) # NOTE: Adapted from falcon.request.Request._parse_form_urlencoded def parse_form_body(req: falcon.Request): if ( req.content_type is not None and "application/x-www-form-urlencoded" in req.content_type ): body = req.stream.read(req.content_length or 0) try: body = body.decode("ascii") except UnicodeDecodeError: body = None req.log_error( "Non-ASCII characters found in form body " "with Content-Type of " "application/x-www-form-urlencoded. Body " "will be ignored." ) if body: return parse_query_string(body, keep_blank=req.options.keep_blank_qs_values) return core.missing class HTTPError(falcon.HTTPError): """HTTPError that stores a dictionary of validation error messages.""" def __init__(self, status, errors, *args, **kwargs): self.errors = errors super().__init__(status, *args, **kwargs) def to_dict(self, *args, **kwargs): """Override `falcon.HTTPError` to include error messages in responses.""" ret = super().to_dict(*args, **kwargs) if self.errors is not None: ret["errors"] = self.errors return ret class FalconParser(core.Parser[falcon.Request]): """Falcon request argument parser. Defaults to using the `media` location. See :py:meth:`~FalconParser.load_media` for details on the media location.""" # by default, Falcon will use the 'media' location to load data # # this effectively looks the same as loading JSON data by default, but if # you add a handler for a different media type to Falcon, webargs will # automatically pick up on that capability DEFAULT_LOCATION = "media" DEFAULT_UNKNOWN_BY_LOCATION = dict( media=ma.RAISE, **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION ) __location_map__ = dict(media="load_media", **core.Parser.__location_map__) # Note on the use of MultiDictProxy throughout: # Falcon parses query strings and form values into ordinary dicts, but with # the values listified where appropriate # it is still therefore necessary in these cases to wrap them in # MultiDictProxy because we need to use the schema to determine when single # values should be wrapped in lists due to the type of the destination # field def load_querystring(self, req: falcon.Request, schema): """Return query params from the request as a MultiDictProxy.""" return self._makeproxy(req.params, schema) def load_form(self, req: falcon.Request, schema): """Return form values from the request as a MultiDictProxy .. note:: The request stream will be read and left at EOF. """ form = parse_form_body(req) if form is core.missing: return form return self._makeproxy(form, schema) def load_media(self, req: falcon.Request, schema): """Return data unpacked and parsed by one of Falcon's media handlers. By default, Falcon only handles JSON payloads. To configure additional media handlers, see the `Falcon documentation on media types`__. .. _FalconMedia: https://falcon.readthedocs.io/en/stable/api/media.html __ FalconMedia_ .. note:: The request stream will be read and left at EOF. """ # if there is no body, return missing instead of erroring if req.content_length in (None, 0): return core.missing return req.media def _raw_load_json(self, req: falcon.Request): """Return a json payload from the request for the core parser's load_json Checks the input mimetype and may return 'missing' if the mimetype is non-json, even if the request body is parseable as json.""" if not is_json_request(req) or req.content_length in (None, 0): return core.missing body = req.stream.read(req.content_length) if body: return core.parse_json(body) return core.missing def load_headers(self, req: falcon.Request, schema): """Return headers from the request.""" # Falcon only exposes headers as a dict (not multidict) return req.headers def load_cookies(self, req: falcon.Request, schema): """Return cookies from the request.""" # Cookies are expressed in Falcon as a dict, but the possibility of # multiple values for a cookie is preserved internally -- if desired in # the future, webargs could add a MultiDict type for Cookies here built # from (req, schema), but Falcon does not provide one out of the box return req.cookies def get_request_from_view_args(self, view, args, kwargs): """Get request from a resource method's arguments. Assumes that request is the second argument. """ req = args[1] if not isinstance(req, falcon.Request): raise TypeError("Argument is not a falcon.Request") return req def load_files(self, req: falcon.Request, schema): raise NotImplementedError( f"Parsing files not yet supported by {self.__class__.__name__}" ) def handle_error( self, error, req: falcon.Request, schema, *, error_status_code, error_headers ): """Handles errors during parsing.""" status = status_map.get(error_status_code or self.DEFAULT_VALIDATION_STATUS) if status is None: raise LookupError(f"Status code {error_status_code} not supported") raise HTTPError(status, errors=error.messages, headers=error_headers) def _handle_invalid_json_error(self, error, req: falcon.Request, *args, **kwargs): status = status_map[400] messages = {"json": ["Invalid JSON body."]} raise HTTPError(status, errors=messages) parser = FalconParser() use_args = parser.use_args use_kwargs = parser.use_kwargs webargs-8.5.0/src/webargs/fields.py000066400000000000000000000111331461250545000172200ustar00rootroot00000000000000"""Field classes. Includes all fields from `marshmallow.fields` in addition to a custom `Nested` field and `DelimitedList`. All fields can optionally take a special `location` keyword argument, which tells webargs where to parse the request argument from. .. code-block:: python args = { "active": fields.Bool(location="query"), "content_type": fields.Str(data_key="Content-Type", location="headers"), } """ from __future__ import annotations import typing import marshmallow as ma # Expose all fields from marshmallow.fields. from marshmallow.fields import * # noqa: F403 __all__ = ["DelimitedList", "DelimitedTuple"] + ma.fields.__all__ # TODO: remove custom `Nested` in the next major release # # the `Nested` class is only needed on versions of marshmallow prior to v3.15.0 # in that version, `ma.fields.Nested` gained the ability to consume dict inputs # prior to that, this subclass adds this capability # # if we drop support for ma.__version_info__ < (3, 15) we can do this class Nested(ma.fields.Nested): # type: ignore[no-redef] """Same as `marshmallow.fields.Nested`, except can be passed a dictionary as the first argument, which will be converted to a `marshmallow.Schema`. .. note:: The schema class here will always be `marshmallow.Schema`, regardless of whether a custom schema class is set on the parser. Pass an explicit schema class if necessary. """ def __init__(self, nested, *args, **kwargs): if isinstance(nested, dict): nested = ma.Schema.from_dict(nested) super().__init__(nested, *args, **kwargs) class DelimitedFieldMixin: """ This is a mixin class for subclasses of ma.fields.List and ma.fields.Tuple which split on a pre-specified delimiter. By default, the delimiter will be "," Because we want the MRO to reach this class before the List or Tuple class, it must be listed first in the superclasses For example, a DelimitedList-like type can be defined like so: >>> class MyDelimitedList(DelimitedFieldMixin, ma.fields.List): >>> pass """ delimiter: str = "," # delimited fields set is_multiple=False for webargs.core.is_multiple is_multiple: bool = False # NOTE: in 8.x this defaults to "" but in 9.x it will be 'missing' empty_value: typing.Any = "" def _serialize(self, value, attr, obj, **kwargs): # serializing will start with parent-class serialization, so that we correctly # output lists of non-primitive types, e.g. DelimitedList(DateTime) return self.delimiter.join( format(each) for each in super()._serialize(value, attr, obj, **kwargs) ) def _deserialize(self, value, attr, data, **kwargs): # attempting to deserialize from a non-string source is an error if not isinstance(value, (str, bytes)): raise self.make_error("invalid") values = value.split(self.delimiter) if value else [] # convert empty strings to the empty value; typically "" and therefore a no-op values = [v or self.empty_value for v in values] return super()._deserialize(values, attr, data, **kwargs) class DelimitedList(DelimitedFieldMixin, ma.fields.List): """A field which is similar to a List, but takes its input as a delimited string (e.g. "foo,bar,baz"). Like List, it can be given a nested field type which it will use to de/serialize each element of the list. :param Field cls_or_instance: A field class or instance. :param str delimiter: Delimiter between values. """ default_error_messages = {"invalid": "Not a valid delimited list."} def __init__( self, cls_or_instance: ma.fields.Field | type, *, delimiter: str | None = None, **kwargs, ): self.delimiter = delimiter or self.delimiter super().__init__(cls_or_instance, **kwargs) class DelimitedTuple(DelimitedFieldMixin, ma.fields.Tuple): """A field which is similar to a Tuple, but takes its input as a delimited string (e.g. "foo,bar,baz"). Like Tuple, it can be given a tuple of nested field types which it will use to de/serialize each element of the tuple. :param Iterable[Field] tuple_fields: An iterable of field classes or instances. :param str delimiter: Delimiter between values. """ default_error_messages = {"invalid": "Not a valid delimited tuple."} def __init__( self, tuple_fields, *, delimiter: str | None = None, **kwargs, ): self.delimiter = delimiter or self.delimiter super().__init__(tuple_fields, **kwargs) webargs-8.5.0/src/webargs/flaskparser.py000066400000000000000000000105101461250545000202650ustar00rootroot00000000000000"""Flask request argument parsing module. Example: :: from flask import Flask from webargs import fields from webargs.flaskparser import use_args app = Flask(__name__) user_detail_args = { 'per_page': fields.Int() } @app.route("/user/") @use_args(user_detail_args) def user_detail(args, uid): return ("The user page for user {uid}, showing {per_page} posts.").format( uid=uid, per_page=args["per_page"] ) """ from __future__ import annotations import json import typing import flask import marshmallow as ma from werkzeug.exceptions import HTTPException from webargs import core def abort( http_status_code: int, exc: Exception | None = None, **kwargs: typing.Any ) -> typing.NoReturn: """Raise a HTTPException for the given http_status_code. Attach any keyword arguments to the exception for later processing. From Flask-Restful. See NOTICE file for license information. """ try: flask.abort(http_status_code) except HTTPException as err: err.data = kwargs # type: ignore err.exc = exc # type: ignore raise err def is_json_request(req: flask.Request) -> bool: return core.is_json(req.mimetype) class FlaskParser(core.Parser[flask.Request]): """Flask request argument parser.""" DEFAULT_UNKNOWN_BY_LOCATION: dict[str, str | None] = { "view_args": ma.RAISE, "path": ma.RAISE, **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION, } __location_map__ = dict( view_args="load_view_args", path="load_view_args", **core.Parser.__location_map__, ) def _raw_load_json(self, req: flask.Request) -> typing.Any: """Return a json payload from the request for the core parser's load_json Checks the input mimetype and may return 'missing' if the mimetype is non-json, even if the request body is parseable as json.""" if not is_json_request(req): return core.missing return core.parse_json(req.get_data(cache=True)) def _handle_invalid_json_error( self, error: json.JSONDecodeError | UnicodeDecodeError, req: flask.Request, *args: typing.Any, **kwargs: typing.Any, ) -> typing.NoReturn: abort(400, exc=error, messages={"json": ["Invalid JSON body."]}) def load_view_args(self, req: flask.Request, schema: ma.Schema) -> typing.Any: """Return the request's ``view_args`` or ``missing`` if there are none.""" return req.view_args or core.missing def load_querystring(self, req: flask.Request, schema: ma.Schema) -> typing.Any: """Return query params from the request as a MultiDictProxy.""" return self._makeproxy(req.args, schema) def load_form(self, req: flask.Request, schema: ma.Schema) -> typing.Any: """Return form values from the request as a MultiDictProxy.""" return self._makeproxy(req.form, schema) def load_headers(self, req: flask.Request, schema: ma.Schema) -> typing.Any: """Return headers from the request as a MultiDictProxy.""" return self._makeproxy(req.headers, schema) def load_cookies(self, req: flask.Request, schema: ma.Schema) -> typing.Any: """Return cookies from the request.""" return req.cookies def load_files(self, req: flask.Request, schema: ma.Schema) -> typing.Any: """Return files from the request as a MultiDictProxy.""" return self._makeproxy(req.files, schema) def handle_error( self, error: ma.ValidationError, req: flask.Request, schema: ma.Schema, *, error_status_code: int | None, error_headers: typing.Mapping[str, str] | None, ) -> typing.NoReturn: """Handles errors during parsing. Aborts the current HTTP request and responds with a 422 error. """ status_code: int = error_status_code or self.DEFAULT_VALIDATION_STATUS abort( status_code, exc=error, messages=error.messages, schema=schema, headers=error_headers, ) def get_default_request(self) -> flask.Request: """Override to use Flask's thread-local request object by default""" return flask.request parser = FlaskParser() use_args = parser.use_args use_kwargs = parser.use_kwargs webargs-8.5.0/src/webargs/multidictproxy.py000066400000000000000000000060001461250545000210470ustar00rootroot00000000000000import typing from collections.abc import Mapping import marshmallow as ma class MultiDictProxy(Mapping): """ A proxy object which wraps multidict types along with a matching schema Whenever a value is looked up, it is checked against the schema to see if there is a matching field where `is_multiple` is True. If there is, then the data should be loaded as a list or tuple. In all other cases, __getitem__ proxies directly to the input multidict. """ def __init__( self, multidict, schema: ma.Schema, known_multi_fields: typing.Tuple[typing.Type, ...] = ( ma.fields.List, ma.fields.Tuple, ), ): self.data = multidict self.known_multi_fields = known_multi_fields self.multiple_keys = self._collect_multiple_keys(schema) def _is_multiple(self, field: ma.fields.Field) -> bool: """Return whether or not `field` handles repeated/multi-value arguments.""" # fields which set `is_multiple = True/False` will have the value selected, # otherwise, we check for explicit criteria is_multiple_attr = getattr(field, "is_multiple", None) if is_multiple_attr is not None: return is_multiple_attr return isinstance(field, self.known_multi_fields) def _collect_multiple_keys(self, schema: ma.Schema): result = set() for name, field in schema.fields.items(): if not self._is_multiple(field): continue result.add(field.data_key if field.data_key is not None else name) return result def __getitem__(self, key): val = self.data.get(key, ma.missing) if val is ma.missing or key not in self.multiple_keys: return val if hasattr(self.data, "getlist"): return self.data.getlist(key) if hasattr(self.data, "getall"): return self.data.getall(key) if isinstance(val, (list, tuple)): return val if val is None: return None return [val] def __str__(self): # str(proxy) proxies to str(proxy.data) return str(self.data) def __repr__(self): return ( f"MultiDictProxy(data={self.data!r}, multiple_keys={self.multiple_keys!r})" ) def __delitem__(self, key): del self.data[key] def __setitem__(self, key, value): self.data[key] = value def __getattr__(self, name): return getattr(self.data, name) def __iter__(self): for x in iter(self.data): # special case for header dicts which produce an iterator of tuples # instead of an iterator of strings if isinstance(x, tuple): yield x[0] else: yield x def __contains__(self, x): return x in self.data def __len__(self): return len(self.data) def __eq__(self, other): return self.data == other def __ne__(self, other): return self.data != other webargs-8.5.0/src/webargs/py.typed000066400000000000000000000000001461250545000170660ustar00rootroot00000000000000webargs-8.5.0/src/webargs/pyramidparser.py000066400000000000000000000164741461250545000206510ustar00rootroot00000000000000"""Pyramid request argument parsing. Example usage: :: from wsgiref.simple_server import make_server from pyramid.config import Configurator from pyramid.response import Response from marshmallow import fields from webargs.pyramidparser import use_args hello_args = { 'name': fields.Str(load_default='World') } @use_args(hello_args) def hello_world(request, args): return Response('Hello ' + args['name']) if __name__ == '__main__': config = Configurator() config.add_route('hello', '/') config.add_view(hello_world, route_name='hello') app = config.make_wsgi_app() server = make_server('0.0.0.0', 6543, app) server.serve_forever() """ from __future__ import annotations import functools from collections.abc import Mapping import marshmallow as ma from pyramid.httpexceptions import exception_response from pyramid.request import Request from webob.multidict import MultiDict from webargs import core from webargs.core import json def is_json_request(req: Request) -> bool: return core.is_json(req.headers.get("content-type")) class PyramidParser(core.Parser[Request]): """Pyramid request argument parser.""" DEFAULT_UNKNOWN_BY_LOCATION: dict[str, str | None] = { "matchdict": ma.RAISE, "path": ma.RAISE, **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION, } __location_map__ = dict( matchdict="load_matchdict", path="load_matchdict", **core.Parser.__location_map__, ) def _raw_load_json(self, req: Request): """Return a json payload from the request for the core parser's load_json Checks the input mimetype and may return 'missing' if the mimetype is non-json, even if the request body is parseable as json.""" if not is_json_request(req): return core.missing return core.parse_json(req.body, encoding=req.charset) def load_querystring(self, req: Request, schema): """Return query params from the request as a MultiDictProxy.""" return self._makeproxy(req.GET, schema) def load_form(self, req: Request, schema): """Return form values from the request as a MultiDictProxy.""" return self._makeproxy(req.POST, schema) def load_cookies(self, req: Request, schema): """Return cookies from the request as a MultiDictProxy.""" return self._makeproxy(req.cookies, schema) def load_headers(self, req: Request, schema): """Return headers from the request as a MultiDictProxy.""" return self._makeproxy(req.headers, schema) def load_files(self, req: Request, schema): """Return files from the request as a MultiDictProxy.""" files = ((k, v) for k, v in req.POST.items() if hasattr(v, "file")) return self._makeproxy(MultiDict(files), schema) def load_matchdict(self, req: Request, schema): """Return the request's ``matchdict`` as a MultiDictProxy.""" return self._makeproxy(req.matchdict, schema) def handle_error( self, error, req: Request, schema, *, error_status_code, error_headers ): """Handles errors during parsing. Aborts the current HTTP request and responds with a 400 error. """ status_code = error_status_code or self.DEFAULT_VALIDATION_STATUS response = exception_response( status_code, detail=str(error), headers=error_headers, content_type="application/json", ) body = json.dumps(error.messages) response.body = body.encode("utf-8") if isinstance(body, str) else body raise response def _handle_invalid_json_error(self, error, req: Request, *args, **kwargs): messages = {"json": ["Invalid JSON body."]} response = exception_response( 400, detail=str(messages), content_type="application/json" ) body = json.dumps(messages) response.body = body.encode("utf-8") if isinstance(body, str) else body raise response def use_args( self, argmap, req: Request | None = None, *, location=core.Parser.DEFAULT_LOCATION, unknown=None, as_kwargs=False, arg_name=None, validate=None, error_status_code=None, error_headers=None, ): """Decorator that injects parsed arguments into a view callable. Supports the *Class-based View* pattern where `request` is saved as an instance attribute on a view class. :param dict argmap: Either a `marshmallow.Schema`, a `dict` of argname -> `marshmallow.fields.Field` pairs, or a callable which accepts a request and returns a `marshmallow.Schema`. :param req: The request object to parse. Pulled off of the view by default. :param str location: Where on the request to load values. :param str unknown: A value to pass for ``unknown`` when calling the schema's ``load`` method. :param bool as_kwargs: Whether to insert arguments as keyword arguments. :param str arg_name: Keyword argument name to use for arguments. Mutually exclusive with as_kwargs. :param callable validate: Validation function that receives the dictionary of parsed arguments. If the function returns ``False``, the parser will raise a :exc:`ValidationError`. :param int error_status_code: Status code passed to error handler functions when a `ValidationError` is raised. :param dict error_headers: Headers passed to error handler functions when a a `ValidationError` is raised. """ location = location or self.location if arg_name is not None and as_kwargs: raise ValueError("arg_name and as_kwargs are mutually exclusive") if arg_name is None and not self.USE_ARGS_POSITIONAL: arg_name = f"{location}_args" # Optimization: If argmap is passed as a dictionary, we only need # to generate a Schema once if isinstance(argmap, Mapping): if not isinstance(argmap, dict): argmap = dict(argmap) argmap = self.schema_class.from_dict(argmap)() def decorator(func): @functools.wraps(func) def wrapper(obj, *args, **kwargs): # The first argument is either `self` or `request` try: # get self.request request = req or obj.request except AttributeError: # first arg is request request = obj # NOTE: At this point, argmap may be a Schema, callable, or dict parsed_args = self.parse( argmap, req=request, location=location, unknown=unknown, validate=validate, error_status_code=error_status_code, error_headers=error_headers, ) args, kwargs = self._update_args_kwargs( args, kwargs, parsed_args, as_kwargs, arg_name ) return func(obj, *args, **kwargs) wrapper.__wrapped__ = func return wrapper return decorator parser = PyramidParser() use_args = parser.use_args use_kwargs = parser.use_kwargs webargs-8.5.0/src/webargs/testing.py000066400000000000000000000232061461250545000174330ustar00rootroot00000000000000"""Utilities for testing. Includes a base test class for testing parsers. .. warning:: Methods and functions in this module may change without warning and without a major version change. """ import pytest import webtest from webargs.core import json class CommonTestCase: """Base test class that defines test methods for common functionality across all parsers. Subclasses must define `create_app`, which returns a WSGI-like app. """ def create_app(self): """Return a WSGI app""" raise NotImplementedError("Must define create_app()") def create_testapp(self, app): return webtest.TestApp(app) def before_create_app(self): pass def after_create_app(self): pass @pytest.fixture(scope="class") def testapp(self): self.before_create_app() yield self.create_testapp(self.create_app()) self.after_create_app() def test_parse_querystring_args(self, testapp): assert testapp.get("/echo?name=Fred").json == {"name": "Fred"} def test_parse_form(self, testapp): assert testapp.post("/echo_form", {"name": "Joe"}).json == {"name": "Joe"} def test_parse_json(self, testapp): assert testapp.post_json("/echo_json", {"name": "Fred"}).json == { "name": "Fred" } def test_parse_json_missing(self, testapp): assert testapp.post("/echo_json", "").json == {"name": "World"} def test_parse_json_or_form(self, testapp): assert testapp.post_json("/echo_json_or_form", {"name": "Fred"}).json == { "name": "Fred" } assert testapp.post("/echo_json_or_form", {"name": "Joe"}).json == { "name": "Joe" } assert testapp.post("/echo_json_or_form", "").json == {"name": "World"} def test_parse_querystring_default(self, testapp): assert testapp.get("/echo").json == {"name": "World"} def test_parse_json_with_charset(self, testapp): res = testapp.post( "/echo_json", json.dumps({"name": "Steve"}), content_type="application/json;charset=UTF-8", ) assert res.json == {"name": "Steve"} def test_parse_json_with_vendor_media_type(self, testapp): res = testapp.post( "/echo_json", json.dumps({"name": "Steve"}), content_type="application/vnd.api+json;charset=UTF-8", ) assert res.json == {"name": "Steve"} def test_parse_ignore_extra_data(self, testapp): assert testapp.post_json( "/echo_ignoring_extra_data", {"extra": "data"} ).json == {"name": "World"} def test_parse_json_empty(self, testapp): assert testapp.post_json("/echo_json", {}).json == {"name": "World"} def test_parse_json_error_unexpected_int(self, testapp): res = testapp.post_json("/echo_json", 1, expect_errors=True) assert res.status_code == 422 def test_parse_json_error_unexpected_list(self, testapp): res = testapp.post_json("/echo_json", [{"extra": "data"}], expect_errors=True) assert res.status_code == 422 def test_parse_json_many_schema_invalid_input(self, testapp): res = testapp.post_json( "/echo_many_schema", [{"name": "a"}], expect_errors=True ) assert res.status_code == 422 def test_parse_json_many_schema(self, testapp): res = testapp.post_json("/echo_many_schema", [{"name": "Steve"}]).json assert res == [{"name": "Steve"}] def test_parse_json_many_schema_error_malformed_data(self, testapp): res = testapp.post_json( "/echo_many_schema", {"extra": "data"}, expect_errors=True ) assert res.status_code == 422 def test_parsing_form_default(self, testapp): assert testapp.post("/echo_form", {}).json == {"name": "World"} def test_parse_querystring_multiple(self, testapp): expected = {"name": ["steve", "Loria"]} assert testapp.get("/echo_multi?name=steve&name=Loria").json == expected # test that passing a single value parses correctly # on parsers like falconparser, where there is no native MultiDict type, # this verifies the usage of MultiDictProxy to ensure that single values # are "listified" def test_parse_querystring_multiple_single_value(self, testapp): expected = {"name": ["steve"]} assert testapp.get("/echo_multi?name=steve").json == expected def test_parse_form_multiple(self, testapp): expected = {"name": ["steve", "Loria"]} assert ( testapp.post("/echo_multi_form", {"name": ["steve", "Loria"]}).json == expected ) def test_parse_json_list(self, testapp): expected = {"name": ["Steve"]} assert ( testapp.post_json("/echo_multi_json", {"name": ["Steve"]}).json == expected ) def test_parse_json_list_error_malformed_data(self, testapp): res = testapp.post_json( "/echo_multi_json", {"name": "Steve"}, expect_errors=True ) assert res.status_code == 422 def test_parse_json_with_nonascii_chars(self, testapp): text = "øˆƒ£ºº∆ƒˆ∆" assert testapp.post_json("/echo_json", {"name": text}).json == {"name": text} # https://github.com/marshmallow-code/webargs/issues/427 def test_parse_json_with_nonutf8_chars(self, testapp): res = testapp.post( "/echo_json", b"\xfe", headers={"Accept": "application/json", "Content-Type": "application/json"}, expect_errors=True, ) assert res.status_code == 400 assert res.json == {"json": ["Invalid JSON body."]} def test_validation_error_returns_422_response(self, testapp): res = testapp.post_json("/echo_json", {"name": "b"}, expect_errors=True) assert res.status_code == 422 def test_user_validation_error_returns_422_response_by_default(self, testapp): res = testapp.post_json("/error", {"text": "foo"}, expect_errors=True) assert res.status_code == 422 def test_use_args_decorator(self, testapp): assert testapp.get("/echo_use_args?name=Fred").json == {"name": "Fred"} def test_use_args_with_path_param(self, testapp): url = "/echo_use_args_with_path_param/foo" res = testapp.get(url + "?value=42") assert res.json == {"value": 42} def test_use_args_with_validation(self, testapp): result = testapp.post("/echo_use_args_validated", {"value": 43}) assert result.status_code == 200 result = testapp.post( "/echo_use_args_validated", {"value": 41}, expect_errors=True ) assert result.status_code == 422 def test_use_kwargs_decorator(self, testapp): assert testapp.get("/echo_use_kwargs?name=Fred").json == {"name": "Fred"} def test_use_kwargs_with_path_param(self, testapp): url = "/echo_use_kwargs_with_path_param/foo" res = testapp.get(url + "?value=42") assert res.json == {"value": 42} def test_parsing_headers(self, testapp): res = testapp.get("/echo_headers", headers={"name": "Fred"}) assert res.json == {"name": "Fred"} def test_parsing_cookies(self, testapp): testapp.set_cookie("name", "Steve") res = testapp.get("/echo_cookie") assert res.json == {"name": "Steve"} def test_parse_nested_json(self, testapp): res = testapp.post_json( "/echo_nested", {"name": {"first": "Steve", "last": "Loria"}} ) assert res.json == {"name": {"first": "Steve", "last": "Loria"}} def test_parse_nested_many_json(self, testapp): in_data = {"users": [{"id": 1, "name": "foo"}, {"id": 2, "name": "bar"}]} res = testapp.post_json("/echo_nested_many", in_data) assert res.json == in_data # Regression test for https://github.com/marshmallow-code/webargs/issues/120 def test_parse_nested_many_missing(self, testapp): in_data = {} res = testapp.post_json("/echo_nested_many", in_data) assert res.json == {} def test_parse_files(self, testapp): res = testapp.post( "/echo_file", {"myfile": webtest.Upload("README.rst", b"data")} ) assert res.json == {"myfile": "data"} # https://github.com/sloria/webargs/pull/297 def test_empty_json(self, testapp): res = testapp.post("/echo_json") assert res.status_code == 200 assert res.json == {"name": "World"} # https://github.com/sloria/webargs/pull/297 def test_empty_json_with_headers(self, testapp): res = testapp.post( "/echo_json", "", headers={"Accept": "application/json", "Content-Type": "application/json"}, ) assert res.status_code == 200 assert res.json == {"name": "World"} # https://github.com/sloria/webargs/issues/329 def test_invalid_json(self, testapp): res = testapp.post( "/echo_json", '{"foo": "bar", }', headers={"Accept": "application/json", "Content-Type": "application/json"}, expect_errors=True, ) assert res.status_code == 400 assert res.json == {"json": ["Invalid JSON body."]} @pytest.mark.parametrize( ("path", "payload", "content_type"), [ ( "/echo_json", json.dumps({"name": "foo"}), "application/x-www-form-urlencoded", ), ("/echo_form", {"name": "foo"}, "application/json"), ], ) def test_content_type_mismatch(self, testapp, path, payload, content_type): res = testapp.post(path, payload, headers={"Content-Type": content_type}) assert res.json == {"name": "World"} webargs-8.5.0/src/webargs/tornadoparser.py000066400000000000000000000126451461250545000206460ustar00rootroot00000000000000"""Tornado request argument parsing module. Example: :: import tornado.web from marshmallow import fields from webargs.tornadoparser import use_args class HelloHandler(tornado.web.RequestHandler): @use_args({'name': fields.Str(load_default='World')}) def get(self, args): response = {'message': 'Hello {}'.format(args['name'])} self.write(response) """ import tornado.concurrent import tornado.web from tornado.escape import _unicode from tornado.httputil import HTTPServerRequest from webargs import core from webargs.multidictproxy import MultiDictProxy class HTTPError(tornado.web.HTTPError): """`tornado.web.HTTPError` that stores validation errors.""" def __init__(self, *args, **kwargs): self.messages = kwargs.pop("messages", {}) self.headers = kwargs.pop("headers", None) super().__init__(*args, **kwargs) def is_json_request(req: HTTPServerRequest): content_type = req.headers.get("Content-Type") return content_type is not None and core.is_json(content_type) class WebArgsTornadoMultiDictProxy(MultiDictProxy): """ Override class for Tornado multidicts, handles argument decoding requirements. """ def __getitem__(self, key): try: value = self.data.get(key, core.missing) if value is core.missing: return core.missing if key in self.multiple_keys: return [ _unicode(v) if isinstance(v, (str, bytes)) else v for v in value ] if value and isinstance(value, (list, tuple)): value = value[0] if isinstance(value, (str, bytes)): return _unicode(value) return value # based on tornado.web.RequestHandler.decode_argument except UnicodeDecodeError as exc: raise HTTPError(400, f"Invalid unicode in {key}: {value[:40]!r}") from exc class WebArgsTornadoCookiesMultiDictProxy(MultiDictProxy): """ And a special override for cookies because they come back as objects with a `value` attribute we need to extract. Also, does not use the `_unicode` decoding step """ def __getitem__(self, key): cookie = self.data.get(key, core.missing) if cookie is core.missing: return core.missing if key in self.multiple_keys: return [cookie.value] return cookie.value class TornadoParser(core.Parser[HTTPServerRequest]): """Tornado request argument parser.""" def _raw_load_json(self, req: HTTPServerRequest): """Return a json payload from the request for the core parser's load_json Checks the input mimetype and may return 'missing' if the mimetype is non-json, even if the request body is parseable as json.""" if not is_json_request(req): return core.missing # request.body may be a concurrent.Future on streaming requests # this would cause a TypeError if we try to parse it if isinstance(req.body, tornado.concurrent.Future): return core.missing return core.parse_json(req.body) def load_querystring(self, req: HTTPServerRequest, schema): """Return query params from the request as a MultiDictProxy.""" return self._makeproxy( req.query_arguments, schema, cls=WebArgsTornadoMultiDictProxy ) def load_form(self, req: HTTPServerRequest, schema): """Return form values from the request as a MultiDictProxy.""" return self._makeproxy( req.body_arguments, schema, cls=WebArgsTornadoMultiDictProxy ) def load_headers(self, req: HTTPServerRequest, schema): """Return headers from the request as a MultiDictProxy.""" return self._makeproxy(req.headers, schema, cls=WebArgsTornadoMultiDictProxy) def load_cookies(self, req: HTTPServerRequest, schema): """Return cookies from the request as a MultiDictProxy.""" # use the specialized subclass specifically for handling Tornado # cookies return self._makeproxy( req.cookies, schema, cls=WebArgsTornadoCookiesMultiDictProxy ) def load_files(self, req: HTTPServerRequest, schema): """Return files from the request as a MultiDictProxy.""" return self._makeproxy(req.files, schema, cls=WebArgsTornadoMultiDictProxy) def handle_error( self, error, req: HTTPServerRequest, schema, *, error_status_code, error_headers ): """Handles errors during parsing. Raises a `tornado.web.HTTPError` with a 400 error. """ status_code = error_status_code or self.DEFAULT_VALIDATION_STATUS if status_code == 422: reason = "Unprocessable Entity" else: reason = None raise HTTPError( status_code, log_message=str(error.messages), reason=reason, messages=error.messages, headers=error_headers, ) def _handle_invalid_json_error( self, error, req: HTTPServerRequest, *args, **kwargs ): raise HTTPError( 400, log_message="Invalid JSON body.", reason="Bad Request", messages={"json": ["Invalid JSON body."]}, ) def get_request_from_view_args(self, view, args, kwargs): return args[0].request parser = TornadoParser() use_args = parser.use_args use_kwargs = parser.use_kwargs webargs-8.5.0/tests/000077500000000000000000000000001461250545000143225ustar00rootroot00000000000000webargs-8.5.0/tests/__init__.py000077500000000000000000000000001461250545000164240ustar00rootroot00000000000000webargs-8.5.0/tests/apps/000077500000000000000000000000001461250545000152655ustar00rootroot00000000000000webargs-8.5.0/tests/apps/__init__.py000066400000000000000000000000001461250545000173640ustar00rootroot00000000000000webargs-8.5.0/tests/apps/aiohttp_app.py000066400000000000000000000163201461250545000201510ustar00rootroot00000000000000import aiohttp import marshmallow as ma from aiohttp.web import json_response from webargs import fields from webargs.aiohttpparser import parser, use_args, use_kwargs from webargs.core import json hello_args = {"name": fields.Str(load_default="World", validate=lambda n: len(n) >= 3)} hello_multiple = {"name": fields.List(fields.Str())} class HelloSchema(ma.Schema): name = fields.Str(load_default="World", validate=lambda n: len(n) >= 3) hello_many_schema = HelloSchema(many=True) # variant which ignores unknown fields hello_exclude_schema = HelloSchema(unknown=ma.EXCLUDE) ##### Handlers ##### async def echo(request): parsed = await parser.parse(hello_args, request, location="query") return json_response(parsed) async def echo_form(request): parsed = await parser.parse(hello_args, request, location="form") return json_response(parsed) async def echo_json(request): try: parsed = await parser.parse(hello_args, request, location="json") except json.JSONDecodeError as exc: raise aiohttp.web.HTTPBadRequest( text=json.dumps(["Invalid JSON."]), content_type="application/json", ) from exc return json_response(parsed) async def echo_json_or_form(request): try: parsed = await parser.parse(hello_args, request, location="json_or_form") except json.JSONDecodeError as exc: raise aiohttp.web.HTTPBadRequest( text=json.dumps(["Invalid JSON."]), content_type="application/json", ) from exc return json_response(parsed) @use_args(hello_args, location="query") async def echo_use_args(request, args): return json_response(args) @use_kwargs(hello_args, location="query") async def echo_use_kwargs(request, name): return json_response({"name": name}) @use_args( {"value": fields.Int()}, validate=lambda args: args["value"] > 42, location="form" ) async def echo_use_args_validated(request, args): return json_response(args) async def echo_ignoring_extra_data(request): return json_response( await parser.parse(hello_exclude_schema, request, unknown=None) ) async def echo_multi(request): parsed = await parser.parse(hello_multiple, request, location="query") return json_response(parsed) async def echo_multi_form(request): parsed = await parser.parse(hello_multiple, request, location="form") return json_response(parsed) async def echo_multi_json(request): parsed = await parser.parse(hello_multiple, request) return json_response(parsed) async def echo_many_schema(request): parsed = await parser.parse(hello_many_schema, request) return json_response(parsed) @use_args({"value": fields.Int()}, location="query") async def echo_use_args_with_path_param(request, args): return json_response(args) @use_kwargs({"value": fields.Int()}, location="query") async def echo_use_kwargs_with_path_param(request, value): return json_response({"value": value}) @use_args({"page": fields.Int(), "q": fields.Int()}, location="query") @use_args({"name": fields.Str()}) async def echo_use_args_multiple(request, query_parsed, json_parsed): return json_response({"query_parsed": query_parsed, "json_parsed": json_parsed}) async def always_error(request): def always_fail(value): raise ma.ValidationError("something went wrong") args = {"text": fields.Str(validate=always_fail)} parsed = await parser.parse(args, request) return json_response(parsed) async def echo_headers(request): parsed = await parser.parse(hello_args, request, location="headers") return json_response(parsed) async def echo_cookie(request): parsed = await parser.parse(hello_args, request, location="cookies") return json_response(parsed) async def echo_nested(request): args = {"name": fields.Nested({"first": fields.Str(), "last": fields.Str()})} parsed = await parser.parse(args, request) return json_response(parsed) async def echo_multiple_args(request): args = {"first": fields.Str(), "last": fields.Str()} parsed = await parser.parse(args, request) return json_response(parsed) async def echo_nested_many(request): args = { "users": fields.Nested({"id": fields.Int(), "name": fields.Str()}, many=True) } parsed = await parser.parse(args, request) return json_response(parsed) async def echo_nested_many_data_key(request): args = { "x_field": fields.Nested({"id": fields.Int()}, many=True, data_key="X-Field") } parsed = await parser.parse(args, request) return json_response(parsed) async def echo_match_info(request): parsed = await parser.parse( {"mymatch": fields.Int()}, request, location="match_info" ) return json_response(parsed) class EchoHandler: @use_args(hello_args, location="query") async def get(self, request, args): return json_response(args) class EchoHandlerView(aiohttp.web.View): @use_args(hello_args, location="query") async def get(self, args): return json_response(args) @use_args(HelloSchema, as_kwargs=True, location="query") async def echo_use_schema_as_kwargs(request, name): return json_response({"name": name}) ##### App factory ##### def add_route(app, methods, route, handler): for method in methods: app.router.add_route(method, route, handler) def create_app(): app = aiohttp.web.Application() add_route(app, ["GET"], "/echo", echo) add_route(app, ["POST"], "/echo_form", echo_form) add_route(app, ["POST"], "/echo_json", echo_json) add_route(app, ["POST"], "/echo_json_or_form", echo_json_or_form) add_route(app, ["GET"], "/echo_use_args", echo_use_args) add_route(app, ["GET"], "/echo_use_kwargs", echo_use_kwargs) add_route(app, ["POST"], "/echo_use_args_validated", echo_use_args_validated) add_route(app, ["POST"], "/echo_ignoring_extra_data", echo_ignoring_extra_data) add_route(app, ["GET"], "/echo_multi", echo_multi) add_route(app, ["POST"], "/echo_multi_form", echo_multi_form) add_route(app, ["POST"], "/echo_multi_json", echo_multi_json) add_route(app, ["GET", "POST"], "/echo_many_schema", echo_many_schema) add_route( app, ["GET", "POST"], "/echo_use_args_with_path_param/{name}", echo_use_args_with_path_param, ) add_route( app, ["GET", "POST"], "/echo_use_kwargs_with_path_param/{name}", echo_use_kwargs_with_path_param, ) add_route(app, ["POST"], "/echo_use_args_multiple", echo_use_args_multiple) add_route(app, ["GET", "POST"], "/error", always_error) add_route(app, ["GET"], "/echo_headers", echo_headers) add_route(app, ["GET"], "/echo_cookie", echo_cookie) add_route(app, ["POST"], "/echo_nested", echo_nested) add_route(app, ["POST"], "/echo_multiple_args", echo_multiple_args) add_route(app, ["POST"], "/echo_nested_many", echo_nested_many) add_route(app, ["POST"], "/echo_nested_many_data_key", echo_nested_many_data_key) add_route(app, ["GET"], "/echo_match_info/{mymatch}", echo_match_info) add_route(app, ["GET"], "/echo_method", EchoHandler().get) add_route(app, ["GET"], "/echo_method_view", EchoHandlerView) add_route(app, ["GET"], "/echo_use_schema_as_kwargs", echo_use_schema_as_kwargs) return app webargs-8.5.0/tests/apps/bottle_app.py000066400000000000000000000076501461250545000200000ustar00rootroot00000000000000import marshmallow as ma from bottle import Bottle, HTTPResponse, debug, request, response from webargs import fields from webargs.bottleparser import parser, use_args, use_kwargs from webargs.core import json hello_args = {"name": fields.Str(load_default="World", validate=lambda n: len(n) >= 3)} hello_multiple = {"name": fields.List(fields.Str())} class HelloSchema(ma.Schema): name = fields.Str(load_default="World", validate=lambda n: len(n) >= 3) hello_many_schema = HelloSchema(many=True) # variant which ignores unknown fields hello_exclude_schema = HelloSchema(unknown=ma.EXCLUDE) app = Bottle() debug(True) @app.route("/echo", method=["GET"]) def echo(): return parser.parse(hello_args, request, location="query") @app.route("/echo_form", method=["POST"]) def echo_form(): return parser.parse(hello_args, location="form") @app.route("/echo_json", method=["POST"]) def echo_json(): return parser.parse(hello_args, location="json") @app.route("/echo_json_or_form", method=["POST"]) def echo_json_or_form(): return parser.parse(hello_args, location="json_or_form") @app.route("/echo_use_args", method=["GET"]) @use_args(hello_args, location="query") def echo_use_args(args): return args @app.route( "/echo_use_args_validated", method=["POST"], apply=use_args( {"value": fields.Int()}, validate=lambda args: args["value"] > 42, location="form", ), ) def echo_use_args_validated(args): return args @app.route("/echo_ignoring_extra_data", method=["POST"]) def echo_json_ignore_extra_data(): return parser.parse(hello_exclude_schema, unknown=None) @app.route( "/echo_use_kwargs", method=["GET"], apply=use_kwargs(hello_args, location="query") ) def echo_use_kwargs(name): return {"name": name} @app.route("/echo_multi", method=["GET"]) def echo_multi(): return parser.parse(hello_multiple, request, location="query") @app.route("/echo_multi_form", method=["POST"]) def multi_form(): return parser.parse(hello_multiple, location="form") @app.route("/echo_multi_json", method=["POST"]) def multi_json(): return parser.parse(hello_multiple) @app.route("/echo_many_schema", method=["POST"]) def echo_many_schema(): arguments = parser.parse(hello_many_schema, request) return HTTPResponse(body=json.dumps(arguments), content_type="application/json") @app.route( "/echo_use_args_with_path_param/", apply=use_args({"value": fields.Int()}, location="query"), ) def echo_use_args_with_path_param(args, name): return args @app.route( "/echo_use_kwargs_with_path_param/", apply=use_kwargs({"value": fields.Int()}, location="query"), ) def echo_use_kwargs_with_path_param(name, value): return {"value": value} @app.route("/error", method=["GET", "POST"]) def always_error(): def always_fail(value): raise ma.ValidationError("something went wrong") args = {"text": fields.Str(validate=always_fail)} return parser.parse(args) @app.route("/echo_headers") def echo_headers(): return parser.parse(hello_args, request, location="headers") @app.route("/echo_cookie") def echo_cookie(): return parser.parse(hello_args, request, location="cookies") @app.route("/echo_file", method=["POST"]) def echo_file(): args = {"myfile": fields.Field()} result = parser.parse(args, location="files") myfile = result["myfile"] content = myfile.file.read().decode("utf8") return {"myfile": content} @app.route("/echo_nested", method=["POST"]) def echo_nested(): args = {"name": fields.Nested({"first": fields.Str(), "last": fields.Str()})} return parser.parse(args) @app.route("/echo_nested_many", method=["POST"]) def echo_nested_many(): args = { "users": fields.Nested({"id": fields.Int(), "name": fields.Str()}, many=True) } return parser.parse(args) @app.error(400) @app.error(422) def handle_error(err): response.content_type = "application/json" return err.body webargs-8.5.0/tests/apps/django_app/000077500000000000000000000000001461250545000173675ustar00rootroot00000000000000webargs-8.5.0/tests/apps/django_app/__init__.py000066400000000000000000000002341461250545000214770ustar00rootroot00000000000000import importlib.metadata DJANGO_MAJOR_VERSION = int(importlib.metadata.version("django").split(".")[0]) DJANGO_SUPPORTS_ASYNC = DJANGO_MAJOR_VERSION >= 3 webargs-8.5.0/tests/apps/django_app/base/000077500000000000000000000000001461250545000203015ustar00rootroot00000000000000webargs-8.5.0/tests/apps/django_app/base/__init__.py000066400000000000000000000000001461250545000224000ustar00rootroot00000000000000webargs-8.5.0/tests/apps/django_app/base/settings.py000066400000000000000000000014731461250545000225200ustar00rootroot00000000000000# Build paths inside the project like this: os.path.join(BASE_DIR, ...) import os BASE_DIR = os.path.dirname(os.path.dirname(__file__)) SECRET_KEY = "s$28!(eonml-m3jgbq_)bj_&#=)sym2d*kx%@j+r&vwusxz%g$" DEBUG = True TEMPLATE_DEBUG = True ALLOWED_HOSTS = ["*"] # Application definition INSTALLED_APPS = ("django.contrib.contenttypes",) MIDDLEWARE_CLASSES = ( "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ) ROOT_URLCONF = "tests.apps.django_app.base.urls" WSGI_APPLICATION = "tests.apps.django_app.base.wsgi.application" LANGUAGE_CODE = "en-us" TIME_ZONE = "UTC" USE_I18N = True USE_L10N = True USE_TZ = True STATIC_URL = "/static/" webargs-8.5.0/tests/apps/django_app/base/urls.py000066400000000000000000000032341461250545000216420ustar00rootroot00000000000000from django.urls import re_path from tests.apps.django_app.echo import views urlpatterns = [ re_path(r"^echo$", views.echo), re_path(r"^async_echo$", views.async_echo), re_path(r"^echo_form$", views.echo_form), re_path(r"^echo_json$", views.echo_json), re_path(r"^echo_json_or_form$", views.echo_json_or_form), re_path(r"^echo_use_args$", views.echo_use_args), re_path(r"^async_echo_use_args$", views.async_echo_use_args), re_path(r"^echo_use_args_validated$", views.echo_use_args_validated), re_path(r"^echo_ignoring_extra_data$", views.echo_ignoring_extra_data), re_path(r"^echo_use_kwargs$", views.echo_use_kwargs), re_path(r"^echo_multi$", views.echo_multi), re_path(r"^echo_multi_form$", views.echo_multi_form), re_path(r"^echo_multi_json$", views.echo_multi_json), re_path(r"^echo_many_schema$", views.echo_many_schema), re_path( r"^echo_use_args_with_path_param/(?P\w+)$", views.echo_use_args_with_path_param, ), re_path( r"^echo_use_kwargs_with_path_param/(?P\w+)$", views.echo_use_kwargs_with_path_param, ), re_path(r"^error$", views.always_error), re_path(r"^echo_headers$", views.echo_headers), re_path(r"^echo_cookie$", views.echo_cookie), re_path(r"^echo_file$", views.echo_file), re_path(r"^echo_nested$", views.echo_nested), re_path(r"^echo_nested_many$", views.echo_nested_many), re_path(r"^echo_cbv$", views.EchoCBV.as_view()), re_path(r"^echo_use_args_cbv$", views.EchoUseArgsCBV.as_view()), re_path( r"^echo_use_args_with_path_param_cbv/(?P\d+)$", views.EchoUseArgsWithParamCBV.as_view(), ), ] webargs-8.5.0/tests/apps/django_app/base/wsgi.py000066400000000000000000000006431461250545000216270ustar00rootroot00000000000000""" WSGI config for helloapp project. It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/1.6/howto/deployment/wsgi/ """ import os os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.apps.django_app.base.settings") from django.core.wsgi import get_wsgi_application # noqa application = get_wsgi_application() webargs-8.5.0/tests/apps/django_app/echo/000077500000000000000000000000001461250545000203055ustar00rootroot00000000000000webargs-8.5.0/tests/apps/django_app/echo/__init__.py000066400000000000000000000000001461250545000224040ustar00rootroot00000000000000webargs-8.5.0/tests/apps/django_app/echo/views.py000066400000000000000000000132141461250545000220150ustar00rootroot00000000000000import asyncio import marshmallow as ma from django.http import HttpResponse from django.views.generic import View from webargs import fields from webargs.core import json from webargs.djangoparser import parser, use_args, use_kwargs hello_args = {"name": fields.Str(load_default="World", validate=lambda n: len(n) >= 3)} hello_multiple = {"name": fields.List(fields.Str())} class HelloSchema(ma.Schema): name = fields.Str(load_default="World", validate=lambda n: len(n) >= 3) hello_many_schema = HelloSchema(many=True) # variant which ignores unknown fields hello_exclude_schema = HelloSchema(unknown=ma.EXCLUDE) def json_response(data, **kwargs): return HttpResponse(json.dumps(data), content_type="application/json", **kwargs) def handle_view_errors(f): if asyncio.iscoroutinefunction(f): async def wrapped(*args, **kwargs): try: return await f(*args, **kwargs) except ma.ValidationError as err: return json_response(err.messages, status=422) except json.JSONDecodeError: return json_response({"json": ["Invalid JSON body."]}, status=400) else: def wrapped(*args, **kwargs): try: return f(*args, **kwargs) except ma.ValidationError as err: return json_response(err.messages, status=422) except json.JSONDecodeError: return json_response({"json": ["Invalid JSON body."]}, status=400) return wrapped @handle_view_errors def echo(request): return json_response(parser.parse(hello_args, request, location="query")) @handle_view_errors async def async_echo(request): return json_response( await parser.async_parse(hello_args, request, location="query") ) @handle_view_errors def echo_form(request): return json_response(parser.parse(hello_args, request, location="form")) @handle_view_errors def echo_json(request): return json_response(parser.parse(hello_args, request, location="json")) @handle_view_errors def echo_json_or_form(request): return json_response(parser.parse(hello_args, request, location="json_or_form")) @handle_view_errors @use_args(hello_args, location="query") def echo_use_args(request, args): return json_response(args) @handle_view_errors @use_args(hello_args, location="query") async def async_echo_use_args(request, args): return json_response(args) @handle_view_errors @use_args( {"value": fields.Int()}, validate=lambda args: args["value"] > 42, location="form" ) def echo_use_args_validated(args): return json_response(args) @handle_view_errors def echo_ignoring_extra_data(request): return json_response(parser.parse(hello_exclude_schema, request, unknown=None)) @handle_view_errors @use_kwargs(hello_args, location="query") def echo_use_kwargs(request, name): return json_response({"name": name}) @handle_view_errors def echo_multi(request): return json_response(parser.parse(hello_multiple, request, location="query")) @handle_view_errors def echo_multi_form(request): return json_response(parser.parse(hello_multiple, request, location="form")) @handle_view_errors def echo_multi_json(request): return json_response(parser.parse(hello_multiple, request)) @handle_view_errors def echo_many_schema(request): return json_response(parser.parse(hello_many_schema, request)) @handle_view_errors @use_args({"value": fields.Int()}, location="query") def echo_use_args_with_path_param(request, args, name): return json_response(args) @handle_view_errors @use_kwargs({"value": fields.Int()}, location="query") def echo_use_kwargs_with_path_param(request, value, name): return json_response({"value": value}) @handle_view_errors def always_error(request): def always_fail(value): raise ma.ValidationError("something went wrong") argmap = {"text": fields.Str(validate=always_fail)} return parser.parse(argmap, request) @handle_view_errors def echo_headers(request): return json_response(parser.parse(hello_args, request, location="headers")) @handle_view_errors def echo_cookie(request): return json_response(parser.parse(hello_args, request, location="cookies")) @handle_view_errors def echo_file(request): args = {"myfile": fields.Field()} result = parser.parse(args, request, location="files") myfile = result["myfile"] content = myfile.read().decode("utf8") return json_response({"myfile": content}) @handle_view_errors def echo_nested(request): argmap = {"name": fields.Nested({"first": fields.Str(), "last": fields.Str()})} return json_response(parser.parse(argmap, request)) @handle_view_errors def echo_nested_many(request): argmap = { "users": fields.Nested({"id": fields.Int(), "name": fields.Str()}, many=True) } return json_response(parser.parse(argmap, request)) class EchoCBV(View): @handle_view_errors def get(self, request): location_kwarg = {} if request.method == "POST" else {"location": "query"} return json_response(parser.parse(hello_args, self.request, **location_kwarg)) post = get class EchoUseArgsCBV(View): @handle_view_errors @use_args(hello_args, location="query") def get(self, request, args): return json_response(args) @handle_view_errors @use_args(hello_args) def post(self, request, args): return json_response(args) class EchoUseArgsWithParamCBV(View): @handle_view_errors @use_args(hello_args, location="query") def get(self, request, args, pid): return json_response(args) @handle_view_errors @use_args(hello_args) def post(self, request, args, pid): return json_response(args) webargs-8.5.0/tests/apps/django_app/manage.py000077500000000000000000000003721461250545000211760ustar00rootroot00000000000000#!/usr/bin/env python import os import sys if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testapp.settings") from django.core.management import execute_from_command_line execute_from_command_line(sys.argv) webargs-8.5.0/tests/apps/falcon_app.py000066400000000000000000000151101461250545000177370ustar00rootroot00000000000000import importlib.metadata import falcon import marshmallow as ma from webargs import fields from webargs.core import json from webargs.falconparser import parser, use_args, use_kwargs hello_args = {"name": fields.Str(load_default="World", validate=lambda n: len(n) >= 3)} hello_multiple = {"name": fields.List(fields.Str())} FALCON_MAJOR_VERSION = int(importlib.metadata.version("falcon").split(".")[0]) FALCON_SUPPORTS_ASYNC = FALCON_MAJOR_VERSION >= 3 class HelloSchema(ma.Schema): name = fields.Str(load_default="World", validate=lambda n: len(n) >= 3) hello_many_schema = HelloSchema(many=True) # variant which ignores unknown fields hello_exclude_schema = HelloSchema(unknown=ma.EXCLUDE) def set_text(resp, value): if FALCON_MAJOR_VERSION >= 3: resp.text = value else: resp.body = value class Echo: def on_get(self, req, resp): parsed = parser.parse(hello_args, req, location="query") set_text(resp, json.dumps(parsed)) class AsyncEcho: async def on_get(self, req, resp): parsed = await parser.async_parse(hello_args, req, location="query") set_text(resp, json.dumps(parsed)) class EchoForm: def on_post(self, req, resp): parsed = parser.parse(hello_args, req, location="form") set_text(resp, json.dumps(parsed)) class EchoJSON: def on_post(self, req, resp): parsed = parser.parse(hello_args, req, location="json") set_text(resp, json.dumps(parsed)) class EchoMedia: def on_post(self, req, resp): parsed = parser.parse(hello_args, req, location="media") set_text(resp, json.dumps(parsed)) class EchoJSONOrForm: def on_post(self, req, resp): parsed = parser.parse(hello_args, req, location="json_or_form") set_text(resp, json.dumps(parsed)) class EchoUseArgs: @use_args(hello_args, location="query") def on_get(self, req, resp, args): set_text(resp, json.dumps(args)) class AsyncEchoUseArgs: @use_args(hello_args, location="query") async def on_get(self, req, resp, args): set_text(resp, json.dumps(args)) class EchoUseKwargs: @use_kwargs(hello_args, location="query") def on_get(self, req, resp, name): set_text(resp, json.dumps({"name": name})) class EchoUseArgsValidated: @use_args( {"value": fields.Int()}, validate=lambda args: args["value"] > 42, location="form", ) def on_post(self, req, resp, args): set_text(resp, json.dumps(args)) class EchoJSONIgnoreExtraData: def on_post(self, req, resp): set_text( resp, json.dumps(parser.parse(hello_exclude_schema, req, unknown=None)) ) class EchoMulti: def on_get(self, req, resp): set_text(resp, json.dumps(parser.parse(hello_multiple, req, location="query"))) class EchoMultiForm: def on_post(self, req, resp): set_text(resp, json.dumps(parser.parse(hello_multiple, req, location="form"))) class EchoMultiJSON: def on_post(self, req, resp): set_text(resp, json.dumps(parser.parse(hello_multiple, req))) class EchoManySchema: def on_post(self, req, resp): set_text(resp, json.dumps(parser.parse(hello_many_schema, req))) class EchoUseArgsWithPathParam: @use_args({"value": fields.Int()}, location="query") def on_get(self, req, resp, args, name): set_text(resp, json.dumps(args)) class EchoUseKwargsWithPathParam: @use_kwargs({"value": fields.Int()}, location="query") def on_get(self, req, resp, value, name): set_text(resp, json.dumps({"value": value})) class AlwaysError: def on_get(self, req, resp): def always_fail(value): raise ma.ValidationError("something went wrong") args = {"text": fields.Str(validate=always_fail)} set_text(resp, json.dumps(parser.parse(args, req))) on_post = on_get class EchoHeaders: def on_get(self, req, resp): class HeaderSchema(ma.Schema): NAME = fields.Str(load_default="World") set_text( resp, json.dumps(parser.parse(HeaderSchema(), req, location="headers")) ) class EchoCookie: def on_get(self, req, resp): set_text(resp, json.dumps(parser.parse(hello_args, req, location="cookies"))) class EchoNested: def on_post(self, req, resp): args = {"name": fields.Nested({"first": fields.Str(), "last": fields.Str()})} set_text(resp, json.dumps(parser.parse(args, req))) class EchoNestedMany: def on_post(self, req, resp): args = { "users": fields.Nested( {"id": fields.Int(), "name": fields.Str()}, many=True ) } set_text(resp, json.dumps(parser.parse(args, req))) def use_args_hook(args, context_key="args", **kwargs): def hook(req, resp, resource, params): parsed_args = parser.parse(args, req=req, **kwargs) req.context[context_key] = parsed_args return hook @falcon.before(use_args_hook(hello_args, location="query")) class EchoUseArgsHook: def on_get(self, req, resp): set_text(resp, json.dumps(req.context["args"])) def create_app(): if FALCON_MAJOR_VERSION >= 3: app = falcon.App() else: app = falcon.API() app.add_route("/echo", Echo()) app.add_route("/echo_form", EchoForm()) app.add_route("/echo_json", EchoJSON()) app.add_route("/echo_media", EchoMedia()) app.add_route("/echo_json_or_form", EchoJSONOrForm()) app.add_route("/echo_use_args", EchoUseArgs()) app.add_route("/echo_use_kwargs", EchoUseKwargs()) app.add_route("/echo_use_args_validated", EchoUseArgsValidated()) app.add_route("/echo_ignoring_extra_data", EchoJSONIgnoreExtraData()) app.add_route("/echo_multi", EchoMulti()) app.add_route("/echo_multi_form", EchoMultiForm()) app.add_route("/echo_multi_json", EchoMultiJSON()) app.add_route("/echo_many_schema", EchoManySchema()) app.add_route("/echo_use_args_with_path_param/{name}", EchoUseArgsWithPathParam()) app.add_route( "/echo_use_kwargs_with_path_param/{name}", EchoUseKwargsWithPathParam() ) app.add_route("/error", AlwaysError()) app.add_route("/echo_headers", EchoHeaders()) app.add_route("/echo_cookie", EchoCookie()) app.add_route("/echo_nested", EchoNested()) app.add_route("/echo_nested_many", EchoNestedMany()) app.add_route("/echo_use_args_hook", EchoUseArgsHook()) return app def create_async_app(): # defer import (async-capable versions only) import falcon.asgi app = falcon.asgi.App() app.add_route("/async_echo", AsyncEcho()) app.add_route("/async_echo_use_args", AsyncEchoUseArgs()) return app webargs-8.5.0/tests/apps/flask_app.py000066400000000000000000000174221461250545000176050ustar00rootroot00000000000000import importlib.metadata import marshmallow as ma from flask import Flask, Response, request from flask import jsonify as J from flask.views import MethodView from webargs import fields from webargs.core import json from webargs.flaskparser import ( parser, use_args, use_kwargs, ) FLASK_MAJOR_VERSION = int(importlib.metadata.version("flask").split(".")[0]) FLASK_SUPPORTS_ASYNC = FLASK_MAJOR_VERSION >= 2 class TestAppConfig: TESTING = True hello_args = {"name": fields.Str(load_default="World", validate=lambda n: len(n) >= 3)} hello_multiple = {"name": fields.List(fields.Str())} class HelloSchema(ma.Schema): name = fields.Str(load_default="World", validate=lambda n: len(n) >= 3) hello_many_schema = HelloSchema(many=True) app = Flask(__name__) app.config.from_object(TestAppConfig) @app.route("/echo", methods=["GET"]) def echo(): return J(parser.parse(hello_args, location="query")) @app.route("/echo_form", methods=["POST"]) def echo_form(): return J(parser.parse(hello_args, location="form")) @app.route("/echo_json", methods=["POST"]) def echo_json(): return J(parser.parse(hello_args, location="json")) @app.route("/echo_json_or_form", methods=["POST"]) def echo_json_or_form(): return J(parser.parse(hello_args, location="json_or_form")) @app.route("/echo_use_args", methods=["GET"]) @use_args(hello_args, location="query") def echo_use_args(args): return J(args) @app.route("/echo_use_args_validated", methods=["POST"]) @use_args( {"value": fields.Int()}, validate=lambda args: args["value"] > 42, location="form" ) def echo_use_args_validated(args): return J(args) @app.route("/echo_ignoring_extra_data", methods=["POST"]) def echo_json_ignore_extra_data(): return J(parser.parse(hello_args, unknown=ma.EXCLUDE)) @app.route("/echo_use_kwargs", methods=["GET"]) @use_kwargs(hello_args, location="query") def echo_use_kwargs(name): return J({"name": name}) @app.route("/echo_multi", methods=["GET"]) def multi(): return J(parser.parse(hello_multiple, location="query")) @app.route("/echo_multi_form", methods=["POST"]) def multi_form(): return J(parser.parse(hello_multiple, location="form")) @app.route("/echo_multi_json", methods=["POST"]) def multi_json(): return J(parser.parse(hello_multiple)) @app.route("/echo_many_schema", methods=["GET", "POST"]) def many_nested(): arguments = parser.parse(hello_many_schema) return Response(json.dumps(arguments), content_type="application/json") @app.route("/echo_use_args_with_path_param/") @use_args({"value": fields.Int()}, location="query") def echo_use_args_with_path(args, name): return J(args) @app.route("/echo_use_kwargs_with_path_param/") @use_kwargs({"value": fields.Int()}, location="query") def echo_use_kwargs_with_path(name, value): return J({"value": value}) @app.route("/error", methods=["GET", "POST"]) def error(): def always_fail(value): raise ma.ValidationError("something went wrong") args = {"text": fields.Str(validate=always_fail)} return J(parser.parse(args)) @app.route("/echo_headers") def echo_headers(): return J(parser.parse(hello_args, location="headers")) # as above, but in this case, turn off the default `EXCLUDE` behavior for # `headers`, so that errors will be raised @app.route("/echo_headers_raising") @use_args(HelloSchema(), location="headers", unknown=None) def echo_headers_raising(args): return J(args) if FLASK_SUPPORTS_ASYNC: @app.route("/echo_headers_raising_async") @use_args(HelloSchema(), location="headers", unknown=None) async def echo_headers_raising_async(args): return J(args) @app.route("/echo_cookie") def echo_cookie(): return J(parser.parse(hello_args, request, location="cookies")) @app.route("/echo_file", methods=["POST"]) def echo_file(): args = {"myfile": fields.Field()} result = parser.parse(args, location="files") fp = result["myfile"] content = fp.read().decode("utf8") return J({"myfile": content}) @app.route("/echo_view_arg/") def echo_view_arg(view_arg): return J(parser.parse({"view_arg": fields.Int()}, location="view_args")) if FLASK_SUPPORTS_ASYNC: @app.route("/echo_view_arg_async/") async def echo_view_arg_async(view_arg): parsed_view_arg = await parser.async_parse( {"view_arg": fields.Int()}, location="view_args" ) return J(parsed_view_arg) @app.route("/echo_view_arg_use_args/") @use_args({"view_arg": fields.Int()}, location="view_args") def echo_view_arg_with_use_args(args, **kwargs): return J(args) if FLASK_SUPPORTS_ASYNC: @app.route("/echo_view_arg_use_args_async/") @use_args({"view_arg": fields.Int()}, location="view_args") async def echo_view_arg_with_use_args_async(args, **kwargs): return J(args) @app.route("/echo_nested", methods=["POST"]) def echo_nested(): args = {"name": fields.Nested({"first": fields.Str(), "last": fields.Str()})} return J(parser.parse(args)) @app.route("/echo_nested_many", methods=["POST"]) def echo_nested_many(): args = { "users": fields.Nested({"id": fields.Int(), "name": fields.Str()}, many=True) } return J(parser.parse(args)) @app.route("/echo_nested_many_data_key", methods=["POST"]) def echo_nested_many_with_data_key(): args = { "x_field": fields.Nested({"id": fields.Int()}, many=True, data_key="X-Field") } return J(parser.parse(args)) if FLASK_SUPPORTS_ASYNC: @app.route("/echo_nested_many_data_key_async", methods=["POST"]) async def echo_nested_many_with_data_key_async(): args = { "x_field": fields.Nested( {"id": fields.Int()}, many=True, data_key="X-Field" ) } return J(await parser.async_parse(args)) class EchoMethodViewUseArgs(MethodView): @use_args({"val": fields.Int()}) def post(self, args): return J(args) app.add_url_rule( "/echo_method_view_use_args", view_func=EchoMethodViewUseArgs.as_view("echo_method_view_use_args"), ) if FLASK_SUPPORTS_ASYNC: class EchoMethodViewUseArgsAsync(MethodView): @use_args({"val": fields.Int()}) async def post(self, args): return J(args) app.add_url_rule( "/echo_method_view_use_args_async", view_func=EchoMethodViewUseArgsAsync.as_view("echo_method_view_use_args_async"), ) class EchoMethodViewUseKwargs(MethodView): @use_kwargs({"val": fields.Int()}) def post(self, val): return J({"val": val}) app.add_url_rule( "/echo_method_view_use_kwargs", view_func=EchoMethodViewUseKwargs.as_view("echo_method_view_use_kwargs"), ) if FLASK_SUPPORTS_ASYNC: class EchoMethodViewUseKwargsAsync(MethodView): @use_kwargs({"val": fields.Int()}) async def post(self, val): return J({"val": val}) app.add_url_rule( "/echo_method_view_use_kwargs_async", view_func=EchoMethodViewUseKwargsAsync.as_view( "echo_method_view_use_kwargs_async" ), ) @app.route("/echo_use_kwargs_missing", methods=["post"]) @use_kwargs({"username": fields.Str(required=True), "password": fields.Str()}) def echo_use_kwargs_missing(username, **kwargs): assert "password" not in kwargs return J({"username": username}) if FLASK_SUPPORTS_ASYNC: @app.route("/echo_use_kwargs_missing_async", methods=["post"]) @use_kwargs({"username": fields.Str(required=True), "password": fields.Str()}) async def echo_use_kwargs_missing_async(username, **kwargs): assert "password" not in kwargs return J({"username": username}) # Return validation errors as JSON @app.errorhandler(422) @app.errorhandler(400) def handle_error(err): if err.code == 422: assert isinstance(err.data["schema"], ma.Schema) return J(err.data["messages"]), err.code webargs-8.5.0/tests/apps/pyramid_app.py000066400000000000000000000134131461250545000201460ustar00rootroot00000000000000import marshmallow as ma from pyramid.config import Configurator from pyramid.httpexceptions import HTTPBadRequest from webargs import fields from webargs.core import json from webargs.pyramidparser import parser, use_args, use_kwargs hello_args = {"name": fields.Str(load_default="World", validate=lambda n: len(n) >= 3)} hello_multiple = {"name": fields.List(fields.Str())} class HelloSchema(ma.Schema): name = fields.Str(load_default="World", validate=lambda n: len(n) >= 3) hello_many_schema = HelloSchema(many=True) # variant which ignores unknown fields hello_exclude_schema = HelloSchema(unknown=ma.EXCLUDE) def echo(request): return parser.parse(hello_args, request, location="query") def echo_form(request): return parser.parse(hello_args, request, location="form") def echo_json(request): try: return parser.parse(hello_args, request, location="json") except json.JSONDecodeError as err: error = HTTPBadRequest() error.body = json.dumps(["Invalid JSON."]).encode("utf-8") error.content_type = "application/json" raise error from err def echo_json_or_form(request): try: return parser.parse(hello_args, request, location="json_or_form") except json.JSONDecodeError as err: error = HTTPBadRequest() error.body = json.dumps(["Invalid JSON."]).encode("utf-8") error.content_type = "application/json" raise error from err def echo_json_ignore_extra_data(request): try: return parser.parse(hello_exclude_schema, request, unknown=None) except json.JSONDecodeError as err: error = HTTPBadRequest() error.body = json.dumps(["Invalid JSON."]).encode("utf-8") error.content_type = "application/json" raise error from err def echo_query(request): return parser.parse(hello_args, request, location="query") @use_args(hello_args, location="query") def echo_use_args(request, args): return args @use_args( {"value": fields.Int()}, validate=lambda args: args["value"] > 42, location="form" ) def echo_use_args_validated(request, args): return args @use_kwargs(hello_args, location="query") def echo_use_kwargs(request, name): return {"name": name} def echo_multi(request): return parser.parse(hello_multiple, request, location="query") def echo_multi_form(request): return parser.parse(hello_multiple, request, location="form") def echo_multi_json(request): return parser.parse(hello_multiple, request) def echo_many_schema(request): return parser.parse(hello_many_schema, request) @use_args({"value": fields.Int()}, location="query") def echo_use_args_with_path_param(request, args): return args @use_kwargs({"value": fields.Int()}, location="query") def echo_use_kwargs_with_path_param(request, value): return {"value": value} def always_error(request): def always_fail(value): raise ma.ValidationError("something went wrong") argmap = {"text": fields.Str(validate=always_fail)} return parser.parse(argmap, request) def echo_headers(request): return parser.parse(hello_args, request, location="headers") def echo_cookie(request): return parser.parse(hello_args, request, location="cookies") def echo_file(request): args = {"myfile": fields.Field()} result = parser.parse(args, request, location="files") myfile = result["myfile"] content = myfile.file.read().decode("utf8") return {"myfile": content} def echo_nested(request): argmap = {"name": fields.Nested({"first": fields.Str(), "last": fields.Str()})} return parser.parse(argmap, request) def echo_nested_many(request): argmap = { "users": fields.Nested({"id": fields.Int(), "name": fields.Str()}, many=True) } return parser.parse(argmap, request) def echo_matchdict(request): return parser.parse({"mymatch": fields.Int()}, request, location="matchdict") class EchoCallable: def __init__(self, request): self.request = request @use_args({"value": fields.Int()}, location="query") def __call__(self, args): return args def add_route(config, route, view, route_name=None, renderer="json"): """Helper for adding a new route-view pair.""" route_name = route_name or view.__name__ config.add_route(route_name, route) config.add_view(view, route_name=route_name, renderer=renderer) def create_app(): config = Configurator() add_route(config, "/echo", echo) add_route(config, "/echo_form", echo_form) add_route(config, "/echo_json", echo_json) add_route(config, "/echo_json_or_form", echo_json_or_form) add_route(config, "/echo_query", echo_query) add_route(config, "/echo_ignoring_extra_data", echo_json_ignore_extra_data) add_route(config, "/echo_use_args", echo_use_args) add_route(config, "/echo_use_args_validated", echo_use_args_validated) add_route(config, "/echo_use_kwargs", echo_use_kwargs) add_route(config, "/echo_multi", echo_multi) add_route(config, "/echo_multi_form", echo_multi_form) add_route(config, "/echo_multi_json", echo_multi_json) add_route(config, "/echo_many_schema", echo_many_schema) add_route( config, "/echo_use_args_with_path_param/{name}", echo_use_args_with_path_param ) add_route( config, "/echo_use_kwargs_with_path_param/{name}", echo_use_kwargs_with_path_param, ) add_route(config, "/error", always_error) add_route(config, "/echo_headers", echo_headers) add_route(config, "/echo_cookie", echo_cookie) add_route(config, "/echo_file", echo_file) add_route(config, "/echo_nested", echo_nested) add_route(config, "/echo_nested_many", echo_nested_many) add_route(config, "/echo_callable", EchoCallable) add_route(config, "/echo_matchdict/{mymatch}", echo_matchdict) return config.make_wsgi_app() webargs-8.5.0/tests/conftest.py000066400000000000000000000001011461250545000165110ustar00rootroot00000000000000import pytest pytest.register_assert_rewrite("webargs.testing") webargs-8.5.0/tests/test_aiohttpparser.py000066400000000000000000000100231461250545000206140ustar00rootroot00000000000000from io import BytesIO from unittest import mock import pytest import webtest import webtest_aiohttp from tests.apps.aiohttp_app import create_app from webargs import fields from webargs.aiohttpparser import AIOHTTPParser from webargs.testing import CommonTestCase @pytest.fixture def web_request(): req = mock.Mock() req.query = {} yield req req.query = {} class TestAIOHTTPParser(CommonTestCase): def create_app(self): return create_app() def create_testapp(self, app, event_loop): return webtest_aiohttp.TestApp(app, loop=event_loop) @pytest.fixture def testapp(self, event_loop): return self.create_testapp(self.create_app(), event_loop) @pytest.mark.skip(reason="files location not supported for aiohttpparser") def test_parse_files(self, testapp): pass def test_parse_match_info(self, testapp): assert testapp.get("/echo_match_info/42").json == {"mymatch": 42} def test_use_args_on_method_handler(self, testapp): assert testapp.get("/echo_method").json == {"name": "World"} assert testapp.get("/echo_method?name=Steve").json == {"name": "Steve"} assert testapp.get("/echo_method_view").json == {"name": "World"} assert testapp.get("/echo_method_view?name=Steve").json == {"name": "Steve"} # regression test for https://github.com/marshmallow-code/webargs/issues/165 def test_multiple_args(self, testapp): res = testapp.post_json("/echo_multiple_args", {"first": "1", "last": "2"}) assert res.json == {"first": "1", "last": "2"} # regression test for https://github.com/marshmallow-code/webargs/issues/145 def test_nested_many_with_data_key(self, testapp): res = testapp.post_json("/echo_nested_many_data_key", {"X-Field": [{"id": 24}]}) assert res.json == {"x_field": [{"id": 24}]} res = testapp.post_json("/echo_nested_many_data_key", {}) assert res.json == {} def test_schema_as_kwargs_view(self, testapp): assert testapp.get("/echo_use_schema_as_kwargs").json == {"name": "World"} assert testapp.get("/echo_use_schema_as_kwargs?name=Chandler").json == { "name": "Chandler" } # https://github.com/marshmallow-code/webargs/pull/297 def test_empty_json_body(self, testapp): environ = {"CONTENT_TYPE": "application/json", "wsgi.input": BytesIO(b"")} req = webtest.TestRequest.blank("/echo", environ) resp = testapp.do_request(req) assert resp.json == {"name": "World"} def test_use_args_multiple(self, testapp): res = testapp.post_json( "/echo_use_args_multiple?page=2&q=10", {"name": "Steve"} ) assert res.json == { "query_parsed": {"page": 2, "q": 10}, "json_parsed": {"name": "Steve"}, } def test_validation_error_returns_422_response(self, testapp): res = testapp.post_json("/echo_json", {"name": "b"}, expect_errors=True) assert res.status_code == 422 assert res.json == {"json": {"name": ["Invalid value."]}} @pytest.mark.asyncio async def test_aiohttpparser_synchronous_error_handler(web_request): parser = AIOHTTPParser() class CustomError(Exception): pass @parser.error_handler def custom_handle_error(error, req, schema, *, error_status_code, error_headers): raise CustomError("foo") with pytest.raises(CustomError): await parser.parse( {"foo": fields.Int(required=True)}, web_request, location="query" ) @pytest.mark.asyncio async def test_aiohttpparser_asynchronous_error_handler(web_request): parser = AIOHTTPParser() class CustomError(Exception): pass @parser.error_handler async def custom_handle_error( error, req, schema, *, error_status_code, error_headers ): async def inner(): raise CustomError("foo") await inner() with pytest.raises(CustomError): await parser.parse( {"foo": fields.Int(required=True)}, web_request, location="query" ) webargs-8.5.0/tests/test_bottleparser.py000066400000000000000000000005261461250545000204440ustar00rootroot00000000000000import pytest from webargs.testing import CommonTestCase from .apps.bottle_app import app class TestBottleParser(CommonTestCase): def create_app(self): return app @pytest.mark.skip(reason="Parsing vendor media types is not supported in bottle") def test_parse_json_with_vendor_media_type(self, testapp): pass webargs-8.5.0/tests/test_core.py000066400000000000000000001626421461250545000166760ustar00rootroot00000000000000import collections import datetime import typing from unittest import mock import pytest from bottle import MultiDict as BotMultiDict from django.utils.datastructures import MultiValueDict as DjMultiDict from marshmallow import ( EXCLUDE, INCLUDE, RAISE, Schema, missing, post_load, pre_load, validates_schema, ) from werkzeug.datastructures import MultiDict as WerkMultiDict from webargs import ValidationError, fields from webargs.core import Parser, get_mimetype, is_json from webargs.multidictproxy import MultiDictProxy class MockHTTPError(Exception): def __init__(self, status_code, headers): self.status_code = status_code self.headers = headers super().__init__(self, "HTTP Error occurred") class MockRequestParser(Parser): """A minimal parser implementation that parses mock requests.""" def load_querystring(self, req, schema): return self._makeproxy(req.query, schema) def load_form(self, req, schema): return MultiDictProxy(req.form, schema) def load_json(self, req, schema): return req.json def load_cookies(self, req, schema): return req.cookies @pytest.fixture(scope="function") def web_request(): req = mock.Mock() req.query = {} yield req req.query = {} @pytest.fixture def parser(): return MockRequestParser() # Parser tests @mock.patch("webargs.core.Parser.load_json") def test_load_json_called_by_parse_default(load_json, web_request): schema = Schema.from_dict({"foo": fields.Field()})() load_json.return_value = {"foo": 1} p = Parser() p.parse(schema, web_request) load_json.assert_called_with(web_request, schema) @pytest.mark.parametrize( "location", ["querystring", "form", "headers", "cookies", "files"] ) def test_load_nondefault_called_by_parse_with_location(location, web_request): with mock.patch( f"webargs.core.Parser.load_{location}" ) as mock_loadfunc, mock.patch("webargs.core.Parser.load_json") as load_json: mock_loadfunc.return_value = {} load_json.return_value = {} p = Parser() # ensure that without location=..., the loader is not called (json is # called) p.parse({"foo": fields.Field()}, web_request) assert mock_loadfunc.call_count == 0 assert load_json.call_count == 1 # but when location=... is given, the loader *is* called and json is # not called p.parse({"foo": fields.Field()}, web_request, location=location) assert mock_loadfunc.call_count == 1 # it was already 1, should not go up assert load_json.call_count == 1 @pytest.mark.asyncio @pytest.mark.parametrize("method", ["parse", "async_parse"]) async def test_parse(parser, web_request, method): web_request.json = {"username": 42, "password": 42} argmap = {"username": fields.Field(), "password": fields.Field()} if method == "async_parse": ret = await parser.async_parse(argmap, web_request) else: ret = parser.parse(argmap, web_request) assert {"username": 42, "password": 42} == ret @pytest.mark.parametrize( "set_location", [ "schema_instance", "parse_call", "parser_default", "parser_class_default", ], ) def test_parse_with_unknown_behavior_specified(parser, web_request, set_location): web_request.json = {"username": 42, "password": 42, "fjords": 42} class CustomSchema(Schema): username = fields.Field() password = fields.Field() def parse_with_desired_behavior(value): if set_location == "schema_instance": if value is not None: # pass 'unknown=None' to parse() in order to indicate that the # schema setting should be respected return parser.parse( CustomSchema(unknown=value), web_request, unknown=None ) else: return parser.parse(CustomSchema(), web_request) elif set_location == "parse_call": return parser.parse(CustomSchema(), web_request, unknown=value) elif set_location == "parser_default": parser.unknown = value return parser.parse(CustomSchema(), web_request) elif set_location == "parser_class_default": class CustomParser(MockRequestParser): DEFAULT_UNKNOWN_BY_LOCATION = {"json": value} return CustomParser().parse(CustomSchema(), web_request) else: raise NotImplementedError # with no unknown setting or unknown=RAISE, it blows up with pytest.raises(ValidationError, match="Unknown field."): parse_with_desired_behavior(None) with pytest.raises(ValidationError, match="Unknown field."): parse_with_desired_behavior(RAISE) # with unknown=EXCLUDE the data is omitted ret = parse_with_desired_behavior(EXCLUDE) assert {"username": 42, "password": 42} == ret # with unknown=INCLUDE it is added even though it isn't part of the schema ret = parse_with_desired_behavior(INCLUDE) assert {"username": 42, "password": 42, "fjords": 42} == ret def test_parse_with_explicit_unknown_overrides_schema(parser, web_request): web_request.json = {"username": 42, "password": 42, "fjords": 42} class CustomSchema(Schema): username = fields.Field() password = fields.Field() # setting RAISE in the parse call overrides schema setting with pytest.raises(ValidationError, match="Unknown field."): parser.parse(CustomSchema(unknown=EXCLUDE), web_request, unknown=RAISE) with pytest.raises(ValidationError, match="Unknown field."): parser.parse(CustomSchema(unknown=INCLUDE), web_request, unknown=RAISE) # and the reverse -- setting EXCLUDE or INCLUDE in the parse call overrides # a schema with RAISE already set ret = parser.parse(CustomSchema(unknown=RAISE), web_request, unknown=EXCLUDE) assert {"username": 42, "password": 42} == ret ret = parser.parse(CustomSchema(unknown=RAISE), web_request, unknown=INCLUDE) assert {"username": 42, "password": 42, "fjords": 42} == ret @pytest.mark.parametrize("clear_method", ["custom_class", "instance_setting", "both"]) def test_parse_with_default_unknown_cleared_uses_schema_value( parser, web_request, clear_method ): web_request.json = {"username": 42, "password": 42, "fjords": 42} class CustomSchema(Schema): username = fields.Field() password = fields.Field() if clear_method == "custom_class": class CustomParser(MockRequestParser): DEFAULT_UNKNOWN_BY_LOCATION = {} parser = CustomParser() elif clear_method == "instance_setting": parser = MockRequestParser(unknown=None) elif clear_method == "both": # setting things in multiple ways should not result in errors class CustomParser(MockRequestParser): DEFAULT_UNKNOWN_BY_LOCATION = {} parser = CustomParser(unknown=None) else: raise NotImplementedError with pytest.raises(ValidationError, match="Unknown field."): parser.parse(CustomSchema(), web_request) with pytest.raises(ValidationError, match="Unknown field."): parser.parse(CustomSchema(unknown=RAISE), web_request) ret = parser.parse(CustomSchema(unknown=EXCLUDE), web_request) assert {"username": 42, "password": 42} == ret ret = parser.parse(CustomSchema(unknown=INCLUDE), web_request) assert {"username": 42, "password": 42, "fjords": 42} == ret @pytest.mark.asyncio @pytest.mark.parametrize("method", ["parse", "async_parse"]) async def test_parse_required_arg_raises_validation_error(parser, web_request, method): web_request.json = {} args = {"foo": fields.Field(required=True)} with pytest.raises(ValidationError, match="Missing data for required field."): if method == "parse": parser.parse(args, web_request) else: await parser.async_parse(args, web_request) def test_arg_not_required_excluded_in_parsed_output(parser, web_request): web_request.json = {"first": "Steve"} args = {"first": fields.Str(), "last": fields.Str()} result = parser.parse(args, web_request) assert result == {"first": "Steve"} def test_arg_allow_none(parser, web_request): web_request.json = {"first": "Steve", "last": None} args = {"first": fields.Str(), "last": fields.Str(allow_none=True)} result = parser.parse(args, web_request) assert result == {"first": "Steve", "last": None} def test_parse_required_arg(parser, web_request): web_request.json = {"foo": 42} result = parser.parse({"foo": fields.Field(required=True)}, web_request) assert result == {"foo": 42} def test_parse_required_list(parser, web_request): web_request.json = {"bar": []} args = {"foo": fields.List(fields.Field(), required=True)} with pytest.raises(ValidationError) as excinfo: parser.parse(args, web_request) assert ( excinfo.value.messages["json"]["foo"][0] == "Missing data for required field." ) # Regression test for https://github.com/marshmallow-code/webargs/issues/107 def test_parse_list_allow_none(parser, web_request): web_request.json = {"foo": None} args = {"foo": fields.List(fields.Field(allow_none=True), allow_none=True)} assert parser.parse(args, web_request) == {"foo": None} def test_parse_list_dont_allow_none(parser, web_request): web_request.json = {"foo": None} args = {"foo": fields.List(fields.Field(), allow_none=False)} with pytest.raises(ValidationError) as excinfo: parser.parse(args, web_request) assert excinfo.value.messages["json"]["foo"][0] == "Field may not be null." def test_parse_empty_list(parser, web_request): web_request.json = {"things": []} args = {"things": fields.List(fields.Field())} assert parser.parse(args, web_request) == {"things": []} def test_parse_missing_list(parser, web_request): web_request.json = {} args = {"things": fields.List(fields.Field())} assert parser.parse(args, web_request) == {} def test_default_location(): assert Parser.DEFAULT_LOCATION == "json" def test_missing_with_default(parser, web_request): web_request.json = {} args = {"val": fields.Field(load_default="pizza")} result = parser.parse(args, web_request) assert result["val"] == "pizza" def test_default_can_be_none(parser, web_request): web_request.json = {} args = {"val": fields.Field(load_default=None, allow_none=True)} result = parser.parse(args, web_request) assert result["val"] is None # Regression test for issue #11 def test_arg_with_default_and_location(parser, web_request): web_request.json = {} args = { "p": fields.Int( load_default=1, validate=lambda p: p > 0, metadata={ "error": "La page demandée n'existe pas", "location": "querystring", }, ) } assert parser.parse(args, web_request) == {"p": 1} def test_value_error_raised_if_parse_called_with_invalid_location(parser, web_request): field = fields.Field() with pytest.raises(ValueError, match="Invalid location argument: invalidlocation"): parser.parse({"foo": field}, web_request, location="invalidlocation") @mock.patch("webargs.core.Parser.handle_error") def test_handle_error_called_when_parsing_raises_error(handle_error, web_request): # handle_error must raise an error to be valid handle_error.side_effect = ValidationError("parsing failed") def always_fail(*args, **kwargs): raise ValidationError("error occurred") p = Parser() assert handle_error.call_count == 0 with pytest.raises(ValidationError): p.parse({"foo": fields.Field()}, web_request, validate=always_fail) assert handle_error.call_count == 1 with pytest.raises(ValidationError): p.parse({"foo": fields.Field()}, web_request, validate=always_fail) assert handle_error.call_count == 2 @pytest.mark.asyncio async def test_handle_error_called_when_async_parsing_raises_error(web_request): with mock.patch("webargs.core.Parser.handle_error") as handle_error: # handle_error must raise an error to be valid handle_error.side_effect = ValidationError("parsing failed") def always_fail(*args, **kwargs): raise ValidationError("error occurred") p = Parser() assert handle_error.call_count == 0 with pytest.raises(ValidationError): await p.async_parse( {"foo": fields.Field()}, web_request, validate=always_fail ) assert handle_error.call_count == 1 with pytest.raises(ValidationError): await p.async_parse( {"foo": fields.Field()}, web_request, validate=always_fail ) assert handle_error.call_count == 2 def test_handle_error_reraises_errors(web_request): p = Parser() with pytest.raises(ValidationError): p.handle_error( ValidationError("error raised"), web_request, Schema(), error_status_code=422, error_headers={}, ) @mock.patch("webargs.core.Parser.load_headers") def test_location_as_init_argument(load_headers, web_request): p = Parser(location="headers") load_headers.return_value = {} p.parse({"foo": fields.Field()}, web_request) assert load_headers.called def test_custom_error_handler(web_request): class CustomError(Exception): pass def error_handler(error, req, schema, *, error_status_code, error_headers): assert isinstance(schema, Schema) raise CustomError(error) def failing_validate_func(args): raise ValidationError("parsing failed") class MySchema(Schema): foo = fields.Int() myschema = MySchema() web_request.json = {"foo": "hello world"} p = Parser(error_handler=error_handler) with pytest.raises(CustomError): p.parse(myschema, web_request, validate=failing_validate_func) def test_custom_error_handler_decorator(web_request): class CustomError(Exception): pass mock_schema = mock.Mock(spec=Schema) mock_schema.strict = True mock_schema.load.side_effect = ValidationError("parsing json failed") parser = Parser() @parser.error_handler def handle_error(error, req, schema, *, error_status_code, error_headers): assert isinstance(schema, Schema) raise CustomError(error) with pytest.raises(CustomError): parser.parse(mock_schema, web_request) @pytest.mark.asyncio @pytest.mark.parametrize("async_handler", [True, False]) async def test_custom_error_handler_decorator_in_async_parse( web_request, async_handler ): class CustomError(Exception): pass mock_schema = mock.Mock(spec=Schema) mock_schema.strict = True mock_schema.load.side_effect = ValidationError("parsing json failed") parser = Parser() if async_handler: @parser.error_handler async def handle_error(error, req, schema, *, error_status_code, error_headers): assert isinstance(schema, Schema) raise CustomError(error) else: @parser.error_handler def handle_error(error, req, schema, *, error_status_code, error_headers): assert isinstance(schema, Schema) raise CustomError(error) with pytest.raises(CustomError): await parser.async_parse(mock_schema, web_request) def test_custom_error_handler_must_reraise(web_request): class CustomError(Exception): pass mock_schema = mock.Mock(spec=Schema) mock_schema.strict = True mock_schema.load.side_effect = ValidationError("parsing json failed") parser = Parser() @parser.error_handler def handle_error(error, req, schema, *, error_status_code, error_headers): pass # because the handler above does not raise a new error, the parser should # raise a ValueError -- indicating a programming error with pytest.raises(ValueError): parser.parse(mock_schema, web_request) def test_custom_location_loader(web_request): web_request.data = {"foo": 42} parser = Parser() @parser.location_loader("data") def load_data(req, schema): return req.data result = parser.parse({"foo": fields.Int()}, web_request, location="data") assert result["foo"] == 42 def test_custom_location_loader_with_data_key(web_request): web_request.data = {"X-Foo": 42} parser = Parser() @parser.location_loader("data") def load_data(req, schema): return req.data result = parser.parse( {"x_foo": fields.Int(data_key="X-Foo")}, web_request, location="data" ) assert result["x_foo"] == 42 def test_full_input_validation(parser, web_request): web_request.json = {"foo": 41, "bar": 42} args = {"foo": fields.Int(), "bar": fields.Int()} with pytest.raises(ValidationError): # Test that `validate` receives dictionary of args parser.parse(args, web_request, validate=lambda args: args["foo"] > args["bar"]) def test_full_input_validation_with_multiple_validators(web_request, parser): def validate1(args): if args["a"] > args["b"]: raise ValidationError("b must be > a") def validate2(args): if args["b"] > args["a"]: raise ValidationError("a must be > b") args = {"a": fields.Int(), "b": fields.Int()} web_request.json = {"a": 2, "b": 1} validators = [validate1, validate2] with pytest.raises(ValidationError, match="b must be > a"): parser.parse(args, web_request, validate=validators) web_request.json = {"a": 1, "b": 2} with pytest.raises(ValidationError, match="a must be > b"): parser.parse(args, web_request, validate=validators) def test_required_with_custom_error(parser, web_request): web_request.json = {} args = { "foo": fields.Str(required=True, error_messages={"required": "We need foo"}) } with pytest.raises(ValidationError) as excinfo: # Test that `validate` receives dictionary of args parser.parse(args, web_request) assert "We need foo" in excinfo.value.messages["json"]["foo"] def test_required_with_custom_error_and_validation_error(parser, web_request): web_request.json = {"foo": ""} args = { "foo": fields.Str( required="We need foo", validate=lambda s: len(s) > 1, error_messages={"validator_failed": "foo required length is 3"}, ) } with pytest.raises(ValidationError) as excinfo: # Test that `validate` receives dictionary of args parser.parse(args, web_request) assert "foo required length is 3" in excinfo.value.args[0]["foo"] def test_full_input_validator_receives_nonascii_input(web_request): def validate(val): return False text = "øœ∑∆∑" web_request.json = {"text": text} parser = MockRequestParser() args = {"text": fields.Str()} with pytest.raises(ValidationError) as excinfo: parser.parse(args, web_request, validate=validate) assert excinfo.value.messages == {"json": ["Invalid value."]} def test_invalid_argument_for_validate(web_request, parser): with pytest.raises(ValueError) as excinfo: parser.parse({}, web_request, validate="notcallable") assert "not a callable or list of callables." in excinfo.value.args[0] def create_bottle_multi_dict(): d = BotMultiDict() d["foos"] = "a" d["foos"] = "b" return d multidicts = [ WerkMultiDict([("foos", "a"), ("foos", "b")]), create_bottle_multi_dict(), DjMultiDict({"foos": ["a", "b"]}), ] @pytest.mark.parametrize("input_dict", multidicts) def test_multidict_proxy(input_dict): class ListSchema(Schema): foos = fields.List(fields.Str()) class StrSchema(Schema): foos = fields.Str() # this MultiDictProxy is aware that "foos" is a list field and will # therefore produce a list with __getitem__ list_wrapped_multidict = MultiDictProxy(input_dict, ListSchema()) # this MultiDictProxy is under the impression that "foos" is just a string # and it should return "a" or "b" # the decision between "a" and "b" in this case belongs to the framework str_wrapped_multidict = MultiDictProxy(input_dict, StrSchema()) assert list_wrapped_multidict["foos"] == ["a", "b"] assert str_wrapped_multidict["foos"] in ("a", "b") def test_parse_with_data_key(web_request): web_request.json = {"Content-Type": "application/json"} parser = MockRequestParser() args = {"content_type": fields.Field(data_key="Content-Type")} parsed = parser.parse(args, web_request) assert parsed == {"content_type": "application/json"} def test_parse_with_data_key_retains_field_name_in_error(web_request): web_request.json = {"Content-Type": 12345} parser = MockRequestParser() args = {"content_type": fields.Str(data_key="Content-Type")} with pytest.raises(ValidationError) as excinfo: parser.parse(args, web_request) assert "json" in excinfo.value.messages assert "Content-Type" in excinfo.value.messages["json"] assert excinfo.value.messages["json"]["Content-Type"] == ["Not a valid string."] def test_parse_nested_with_data_key(web_request): parser = MockRequestParser() web_request.json = {"nested_arg": {"wrong": "OK"}} args = {"nested_arg": fields.Nested({"right": fields.Field(data_key="wrong")})} parsed = parser.parse(args, web_request) assert parsed == {"nested_arg": {"right": "OK"}} def test_parse_nested_with_missing_key_and_data_key(web_request): parser = MockRequestParser() web_request.json = {"nested_arg": {}} args = { "nested_arg": fields.Nested( {"found": fields.Field(load_default=None, allow_none=True, data_key="miss")} ) } parsed = parser.parse(args, web_request) assert parsed == {"nested_arg": {"found": None}} def test_parse_nested_with_default(web_request): parser = MockRequestParser() web_request.json = {"nested_arg": {}} args = {"nested_arg": fields.Nested({"miss": fields.Field(load_default="")})} parsed = parser.parse(args, web_request) assert parsed == {"nested_arg": {"miss": ""}} def test_nested_many(web_request, parser): web_request.json = {"pets": [{"name": "Pips"}, {"name": "Zula"}]} args = {"pets": fields.Nested({"name": fields.Str()}, required=True, many=True)} parsed = parser.parse(args, web_request) assert parsed == {"pets": [{"name": "Pips"}, {"name": "Zula"}]} web_request.json = {} with pytest.raises(ValidationError): parser.parse(args, web_request) def test_use_args(web_request, parser): user_args = {"username": fields.Str(), "password": fields.Str()} web_request.json = {"username": "foo", "password": "bar"} @parser.use_args(user_args, web_request) def viewfunc(args): return args assert viewfunc() == {"username": "foo", "password": "bar"} @pytest.mark.asyncio async def test_use_args_on_async(web_request, parser): user_args = {"username": fields.Str(), "password": fields.Str()} web_request.json = {"username": "foo", "password": "bar"} @parser.use_args(user_args, web_request) async def viewfunc(args): return args data = await viewfunc() assert data == {"username": "foo", "password": "bar"} def test_use_args_stacked(web_request, parser): query_args = {"page": fields.Int()} json_args = {"username": fields.Str()} web_request.json = {"username": "foo"} web_request.query = {"page": 42} @parser.use_args(query_args, web_request, location="query") @parser.use_args(json_args, web_request) def viewfunc(query_parsed, json_parsed): return {"json": json_parsed, "query": query_parsed} assert viewfunc() == {"json": {"username": "foo"}, "query": {"page": 42}} def test_use_args_forbids_invalid_usages(parser): with pytest.raises( ValueError, match="arg_name and as_kwargs are mutually exclusive" ): parser.use_args({}, web_request, arg_name="foo", as_kwargs=True) def test_use_kwargs_stacked(web_request, parser): query_args = { "page": fields.Int(error_messages={"invalid": "{input} not a valid integer"}) } json_args = {"username": fields.Str()} web_request.json = {"username": "foo"} web_request.query = {"page": 42} @parser.use_kwargs(query_args, web_request, location="query") @parser.use_kwargs(json_args, web_request) def viewfunc(page, username): return {"json": {"username": username}, "query": {"page": page}} assert viewfunc() == {"json": {"username": "foo"}, "query": {"page": 42}} @pytest.mark.parametrize("decorator_name", ["use_args", "use_kwargs"]) def test_decorators_dont_change_docstring(parser, decorator_name): decorator = getattr(parser, decorator_name) @decorator({"val": fields.Int()}) def viewfunc(*args, **kwargs): """View docstring""" pass assert viewfunc.__doc__ == "View docstring" def test_list_allowed_missing(web_request, parser): args = {"name": fields.List(fields.Str())} web_request.json = {} result = parser.parse(args, web_request) assert result == {} def test_int_list_allowed_missing(web_request, parser): args = {"name": fields.List(fields.Int())} web_request.json = {} result = parser.parse(args, web_request) assert result == {} def test_multiple_arg_required_with_int_conversion(web_request, parser): args = {"ids": fields.List(fields.Int(), required=True)} web_request.json = {} with pytest.raises(ValidationError) as excinfo: parser.parse(args, web_request) assert excinfo.value.messages == { "json": {"ids": ["Missing data for required field."]} } def test_parse_with_callable(web_request, parser): web_request.json = {"foo": 42} class MySchema(Schema): foo = fields.Field() def make_schema(req): assert req is web_request return MySchema(context={"request": req}) result = parser.parse(make_schema, web_request) assert result == {"foo": 42} def test_use_args_callable(web_request, parser): class HelloSchema(Schema): name = fields.Str() @post_load def request_data(self, item, **kwargs): item["data"] = self.context["request"].data return item web_request.json = {"name": "foo"} web_request.data = "request-data" def make_schema(req): assert req is web_request return HelloSchema(context={"request": req}) @parser.use_args(make_schema, web_request) def viewfunc(args): return args assert viewfunc() == {"name": "foo", "data": "request-data"} class TestPassingSchema: class UserSchema(Schema): id = fields.Int(dump_only=True) email = fields.Email() password = fields.Str(load_only=True) def test_passing_schema_to_parse(self, parser, web_request): web_request.json = {"email": "foo@bar.com", "password": "bar"} result = parser.parse(self.UserSchema(), web_request) assert result == {"email": "foo@bar.com", "password": "bar"} def test_use_args_can_be_passed_a_schema(self, web_request, parser): web_request.json = {"email": "foo@bar.com", "password": "bar"} @parser.use_args(self.UserSchema(), web_request) def viewfunc(args): return args assert viewfunc() == {"email": "foo@bar.com", "password": "bar"} def test_passing_schema_factory_to_parse(self, parser, web_request): web_request.json = {"email": "foo@bar.com", "password": "bar"} def factory(req): assert req is web_request return self.UserSchema(context={"request": req}) result = parser.parse(factory, web_request) assert result == {"email": "foo@bar.com", "password": "bar"} def test_use_args_can_be_passed_a_schema_factory(self, web_request, parser): web_request.json = {"email": "foo@bar.com", "password": "bar"} def factory(req): assert req is web_request return self.UserSchema(context={"request": req}) @parser.use_args(factory, web_request) def viewfunc(args): return args assert viewfunc() == {"email": "foo@bar.com", "password": "bar"} def test_use_kwargs_can_be_passed_a_schema(self, web_request, parser): web_request.json = {"email": "foo@bar.com", "password": "bar"} @parser.use_kwargs(self.UserSchema(), web_request) def viewfunc(email, password): return {"email": email, "password": password} assert viewfunc() == {"email": "foo@bar.com", "password": "bar"} def test_use_kwargs_can_be_passed_a_schema_factory(self, web_request, parser): web_request.json = {"email": "foo@bar.com", "password": "bar"} def factory(req): assert req is web_request return self.UserSchema(context={"request": req}) @parser.use_kwargs(factory, web_request) def viewfunc(email, password): return {"email": email, "password": password} assert viewfunc() == {"email": "foo@bar.com", "password": "bar"} def test_use_kwargs_stacked(self, web_request, parser): web_request.json = {"email": "foo@bar.com", "password": "bar", "page": 42} @parser.use_kwargs({"page": fields.Int()}, web_request, unknown=EXCLUDE) @parser.use_kwargs(self.UserSchema(), web_request, unknown=EXCLUDE) def viewfunc(email, password, page): return {"email": email, "password": password, "page": page} assert viewfunc() == {"email": "foo@bar.com", "password": "bar", "page": 42} # Regression test for https://github.com/marshmallow-code/webargs/issues/146 def test_parse_does_not_add_missing_values_to_schema_validator( self, web_request, parser ): class UserSchema(Schema): name = fields.Str() location = fields.Field(required=False) @validates_schema(pass_original=True) def validate_schema(self, data, original_data, **kwargs): assert "location" not in original_data return True web_request.json = {"name": "Eric Cartman"} res = parser.parse(UserSchema, web_request) assert res == {"name": "Eric Cartman"} def test_use_args_with_custom_location_in_parser(web_request, parser): custom_args = {"foo": fields.Str()} web_request.json = {} parser.location = "custom" @parser.location_loader("custom") def load_custom(schema, req): return {"foo": "bar"} @parser.use_args(custom_args, web_request) def viewfunc(args): return args assert viewfunc() == {"foo": "bar"} def test_use_kwargs(web_request, parser): user_args = {"username": fields.Str(), "password": fields.Str()} web_request.json = {"username": "foo", "password": "bar"} @parser.use_kwargs(user_args, web_request) def viewfunc(username, password): return {"username": username, "password": password} assert viewfunc() == {"username": "foo", "password": "bar"} def test_use_kwargs_with_arg_missing(web_request, parser): user_args = {"username": fields.Str(required=True), "password": fields.Str()} web_request.json = {"username": "foo"} @parser.use_kwargs(user_args, web_request) def viewfunc(username, **kwargs): assert "password" not in kwargs return {"username": username} assert viewfunc() == {"username": "foo"} def test_delimited_list_empty_string(web_request, parser): web_request.json = {"dates": ""} schema_cls = Schema.from_dict({"dates": fields.DelimitedList(fields.Str())}) schema = schema_cls() parsed = parser.parse(schema, web_request) assert parsed["dates"] == [] data = schema.dump(parsed) assert data["dates"] == "" def test_delimited_list_default_delimiter(web_request, parser): web_request.json = {"ids": "1,2,3"} schema_cls = Schema.from_dict({"ids": fields.DelimitedList(fields.Int())}) schema = schema_cls() parsed = parser.parse(schema, web_request) assert parsed["ids"] == [1, 2, 3] data = schema.dump(parsed) assert data["ids"] == "1,2,3" def test_delimited_tuple_default_delimiter(web_request, parser): """ Test load and dump from DelimitedTuple, including the use of a datetime type (similar to a DelimitedList test below) which confirms that we aren't relying on __str__, but are properly de/serializing the included fields """ web_request.json = {"ids": "1,2,2020-05-04"} schema_cls = Schema.from_dict( { "ids": fields.DelimitedTuple( (fields.Int, fields.Int, fields.DateTime(format="%Y-%m-%d")) ) } ) schema = schema_cls() parsed = parser.parse(schema, web_request) assert parsed["ids"] == (1, 2, datetime.datetime(2020, 5, 4)) data = schema.dump(parsed) assert data["ids"] == "1,2,2020-05-04" def test_delimited_tuple_incorrect_arity(web_request, parser): web_request.json = {"ids": "1,2"} schema_cls = Schema.from_dict( {"ids": fields.DelimitedTuple((fields.Int, fields.Int, fields.Int))} ) schema = schema_cls() with pytest.raises(ValidationError): parser.parse(schema, web_request) def test_delimited_list_with_datetime(web_request, parser): """ Test that DelimitedList(DateTime(format=...)) correctly parses and dumps dates to and from strings -- indicates that we're doing proper serialization of values in dump() and not just relying on __str__ producing correct results """ web_request.json = {"dates": "2018-11-01,2018-11-02"} schema_cls = Schema.from_dict( {"dates": fields.DelimitedList(fields.DateTime(format="%Y-%m-%d"))} ) schema = schema_cls() parsed = parser.parse(schema, web_request) assert parsed["dates"] == [ datetime.datetime(2018, 11, 1), datetime.datetime(2018, 11, 2), ] data = schema.dump(parsed) assert data["dates"] == "2018-11-01,2018-11-02" def test_delimited_list_custom_delimiter(web_request, parser): web_request.json = {"ids": "1|2|3"} schema_cls = Schema.from_dict( {"ids": fields.DelimitedList(fields.Int(), delimiter="|")} ) schema = schema_cls() parsed = parser.parse(schema, web_request) assert parsed["ids"] == [1, 2, 3] data = schema.dump(parsed) assert data["ids"] == "1|2|3" def test_delimited_tuple_custom_delimiter(web_request, parser): web_request.json = {"ids": "1|2"} schema_cls = Schema.from_dict( {"ids": fields.DelimitedTuple((fields.Int, fields.Int), delimiter="|")} ) schema = schema_cls() parsed = parser.parse(schema, web_request) assert parsed["ids"] == (1, 2) data = schema.dump(parsed) assert data["ids"] == "1|2" def test_delimited_list_load_list_errors(web_request, parser): web_request.json = {"ids": [1, 2, 3]} schema_cls = Schema.from_dict({"ids": fields.DelimitedList(fields.Int())}) schema = schema_cls() with pytest.raises(ValidationError) as excinfo: parser.parse(schema, web_request) exc = excinfo.value assert isinstance(exc, ValidationError) errors = exc.args[0] assert errors["ids"] == ["Not a valid delimited list."] def test_delimited_tuple_load_list_errors(web_request, parser): web_request.json = {"ids": [1, 2]} schema_cls = Schema.from_dict( {"ids": fields.DelimitedTuple((fields.Int, fields.Int))} ) schema = schema_cls() with pytest.raises(ValidationError) as excinfo: parser.parse(schema, web_request) exc = excinfo.value assert isinstance(exc, ValidationError) errors = exc.args[0] assert errors["ids"] == ["Not a valid delimited tuple."] # Regresion test for https://github.com/marshmallow-code/webargs/issues/149 def test_delimited_list_passed_invalid_type(web_request, parser): web_request.json = {"ids": 1} schema_cls = Schema.from_dict({"ids": fields.DelimitedList(fields.Int())}) schema = schema_cls() with pytest.raises(ValidationError) as excinfo: parser.parse(schema, web_request) assert excinfo.value.messages == {"json": {"ids": ["Not a valid delimited list."]}} def test_delimited_tuple_passed_invalid_type(web_request, parser): web_request.json = {"ids": 1} schema_cls = Schema.from_dict({"ids": fields.DelimitedTuple((fields.Int,))}) schema = schema_cls() with pytest.raises(ValidationError) as excinfo: parser.parse(schema, web_request) assert excinfo.value.messages == {"json": {"ids": ["Not a valid delimited tuple."]}} def test_delimited_list_custom_empty_value(web_request, parser): class ZeroList(fields.DelimitedList): empty_value = 0 web_request.json = {"ids": "1,,3"} schema_cls = Schema.from_dict({"ids": ZeroList(fields.Int())}) schema = schema_cls() parsed = parser.parse(schema, web_request) assert parsed["ids"] == [1, 0, 3] def test_delimited_tuple_custom_empty_value(web_request, parser): class ZeroTuple(fields.DelimitedTuple): empty_value = 0 web_request.json = {"ids": "1,,3"} schema_cls = Schema.from_dict( {"ids": ZeroTuple((fields.Int, fields.Int, fields.Int))} ) schema = schema_cls() parsed = parser.parse(schema, web_request) assert parsed["ids"] == (1, 0, 3) def test_delimited_list_using_missing_for_empty(web_request, parser): # this is "future" because we plan to make this the default for webargs v9.0 class FutureList(fields.DelimitedList): empty_value = missing web_request.json = {"ids": "foo,,bar"} schema_cls = Schema.from_dict( {"ids": FutureList(fields.String(load_default="nil"))} ) schema = schema_cls() parsed = parser.parse(schema, web_request) assert parsed["ids"] == ["foo", "nil", "bar"] def test_missing_list_argument_not_in_parsed_result(web_request, parser): # arg missing in request web_request.json = {} args = {"ids": fields.List(fields.Int())} result = parser.parse(args, web_request) assert "ids" not in result def test_type_conversion_with_multiple_required(web_request, parser): web_request.json = {} args = {"ids": fields.List(fields.Int(), required=True)} msg = "Missing data for required field." with pytest.raises(ValidationError, match=msg): parser.parse(args, web_request) @pytest.mark.parametrize("input_dict", multidicts) @pytest.mark.parametrize( "setting", [ "is_multiple_true", "is_multiple_false", "is_multiple_notset", "list_field", "tuple_field", "added_to_known", ], ) def test_is_multiple_detection(web_request, parser, input_dict, setting): # this custom class "multiplexes" in that it can be given a single value or # list of values -- a single value is treated as a string, and a list of # values is treated as a list of strings class CustomMultiplexingField(fields.String): def _deserialize(self, value, attr, data, **kwargs): if isinstance(value, str): return super()._deserialize(value, attr, data, **kwargs) return [ self._deserialize(v, attr, data, **kwargs) for v in value if isinstance(v, str) ] def _serialize(self, value, attr, **kwargs): if isinstance(value, str): return super()._serialize(value, attr, **kwargs) return [ self._serialize(v, attr, **kwargs) for v in value if isinstance(v, str) ] class CustomMultipleField(CustomMultiplexingField): is_multiple = True class CustomNonMultipleField(CustomMultiplexingField): is_multiple = False # the request's query params are the input multidict web_request.query = input_dict # case 1: is_multiple=True if setting == "is_multiple_true": # the multidict should unpack to a list of strings # # order is not necessarily guaranteed by the multidict implementations, but # both values must be present args = {"foos": CustomMultipleField()} result = parser.parse(args, web_request, location="query") assert result["foos"] in (["a", "b"], ["b", "a"]) # case 2: is_multiple=False elif setting == "is_multiple_false": # the multidict should unpack to a string # # either value may be returned, depending on the multidict implementation, # but not both args = {"foos": CustomNonMultipleField()} result = parser.parse(args, web_request, location="query") assert result["foos"] in ("a", "b") # case 3: is_multiple is not set elif setting == "is_multiple_notset": # this should be the same as is_multiple=False args = {"foos": CustomMultiplexingField()} result = parser.parse(args, web_request, location="query") assert result["foos"] in ("a", "b") # case 4: the field is a List (special case) elif setting == "list_field": # this should behave like the is_multiple=True case args = {"foos": fields.List(fields.Str())} result = parser.parse(args, web_request, location="query") assert result["foos"] in (["a", "b"], ["b", "a"]) # case 5: the field is a Tuple (special case) elif setting == "tuple_field": # this should behave like the is_multiple=True case and produce a tuple args = {"foos": fields.Tuple((fields.Str, fields.Str))} result = parser.parse(args, web_request, location="query") assert result["foos"] in (("a", "b"), ("b", "a")) # case 6: the field is custom, but added to the known fields of the parser elif setting == "added_to_known": # if it's included in the known multifields and is_multiple is not set, behave # like is_multiple=True parser.KNOWN_MULTI_FIELDS.append(CustomMultiplexingField) args = {"foos": CustomMultiplexingField()} result = parser.parse(args, web_request, location="query") assert result["foos"] in (["a", "b"], ["b", "a"]) else: raise NotImplementedError def test_validation_errors_in_validator_are_passed_to_handle_error(parser, web_request): def validate(value): raise ValidationError("Something went wrong.") args = {"name": fields.Field(validate=validate, metadata={"location": "json"})} web_request.json = {"name": "invalid"} with pytest.raises(ValidationError) as excinfo: parser.parse(args, web_request) exc = excinfo.value assert isinstance(exc, ValidationError) errors = exc.args[0] assert errors["name"] == ["Something went wrong."] def test_parse_basic(web_request, parser): web_request.json = {"foo": "42"} args = {"foo": fields.Int()} result = parser.parse(args, web_request) assert result == {"foo": 42} def test_parse_raises_validation_error_if_data_invalid(web_request, parser): args = {"email": fields.Email()} web_request.json = {"email": "invalid"} with pytest.raises(ValidationError): parser.parse(args, web_request) def test_nested_field_from_dict(): # webargs.fields.Nested implements dict handling argmap = {"nest": fields.Nested({"foo": fields.Field()})} schema_cls = Schema.from_dict(argmap) assert issubclass(schema_cls, Schema) schema = schema_cls() assert "nest" in schema.fields assert type(schema.fields["nest"]) is fields.Nested assert "foo" in schema.fields["nest"].schema.fields def test_is_json(): assert is_json(None) is False assert is_json("application/json") is True assert is_json("application/xml") is False assert is_json("application/vnd.api+json") is True def test_get_mimetype(): assert get_mimetype("application/json") == "application/json" assert get_mimetype("application/json;charset=utf8") == "application/json" class MockRequestParserWithErrorHandler(MockRequestParser): def handle_error(self, error, req, schema, *, error_status_code, error_headers): assert isinstance(error, ValidationError) assert isinstance(schema, Schema) raise MockHTTPError(error_status_code, error_headers) def test_parse_with_error_status_code_and_headers(web_request): parser = MockRequestParserWithErrorHandler() web_request.json = {"foo": 42} args = {"foo": fields.Field(validate=lambda x: False)} with pytest.raises(MockHTTPError) as excinfo: parser.parse( args, web_request, error_status_code=418, error_headers={"X-Foo": "bar"} ) error = excinfo.value assert error.status_code == 418 assert error.headers == {"X-Foo": "bar"} @mock.patch("webargs.core.Parser.load_json") def test_custom_schema_class(load_json, web_request): class CustomSchema(Schema): @pre_load def pre_load(self, data, **kwargs): data["value"] += " world" return data load_json.return_value = {"value": "hello"} argmap = {"value": fields.Str()} p = Parser(schema_class=CustomSchema) ret = p.parse(argmap, web_request) assert ret == {"value": "hello world"} @mock.patch("webargs.core.Parser.load_json") def test_custom_default_schema_class(load_json, web_request): class CustomSchema(Schema): @pre_load def pre_load(self, data, **kwargs): data["value"] += " world" return data class CustomParser(Parser): DEFAULT_SCHEMA_CLASS = CustomSchema load_json.return_value = {"value": "hello"} argmap = {"value": fields.Str()} p = CustomParser() ret = p.parse(argmap, web_request) assert ret == {"value": "hello world"} def test_parser_pre_load(web_request): class CustomParser(MockRequestParser): # pre-load hook to strip whitespace from query params def pre_load(self, data, *, schema, req, location): if location == "query": return {k: v.strip() for k, v in data.items()} return data parser = CustomParser() # mock data for both query and json web_request.query = web_request.json = {"value": " hello "} argmap = {"value": fields.Str()} # data gets through for 'json' just fine ret = parser.parse(argmap, web_request) assert ret == {"value": " hello "} # but for 'query', the pre_load hook changes things ret = parser.parse(argmap, web_request, location="query") assert ret == {"value": "hello"} # this test is meant to be a run of the WhitspaceStrippingFlaskParser we give # in the docs/advanced.rst examples for how to use pre_load # this helps ensure that the example code is correct # rather than a FlaskParser, we're working with the mock parser, but it's # otherwise the same def test_whitespace_stripping_parser_example(web_request): def _strip_whitespace(value): if isinstance(value, str): value = value.strip() elif isinstance(value, typing.Mapping): return {k: _strip_whitespace(value[k]) for k in value} elif isinstance(value, (list, tuple)): return type(value)(map(_strip_whitespace, value)) return value class WhitspaceStrippingParser(MockRequestParser): def pre_load(self, location_data, *, schema, req, location): if location in ("query", "form"): ret = _strip_whitespace(location_data) return ret return location_data parser = WhitspaceStrippingParser() # mock data for query, form, and json web_request.form = web_request.query = web_request.json = {"value": " hello "} argmap = {"value": fields.Str()} # data gets through for 'json' just fine ret = parser.parse(argmap, web_request) assert ret == {"value": " hello "} # but for 'query' and 'form', the pre_load hook changes things for loc in ("query", "form"): ret = parser.parse(argmap, web_request, location=loc) assert ret == {"value": "hello"} # check that it applies in the case where the field is a list type # applied to an argument (logic for `tuple` is effectively the same) web_request.form = web_request.query = web_request.json = { "ids": [" 1", "3", " 4"], "values": [" foo ", " bar"], } schema = Schema.from_dict( {"ids": fields.List(fields.Int), "values": fields.List(fields.Str)} ) for loc in ("query", "form"): ret = parser.parse(schema, web_request, location=loc) assert ret == {"ids": [1, 3, 4], "values": ["foo", "bar"]} # json loading should also work even though the pre_load hook above # doesn't strip whitespace from JSON data # - values=[" foo ", ...] will have whitespace preserved # - ids=[" 1", ...] will still parse okay because " 1" is valid for fields.Int ret = parser.parse(schema, web_request, location="json") assert ret == {"ids": [1, 3, 4], "values": [" foo ", " bar"]} def test_parse_allows_non_dict_argmap_mapping(parser, web_request): web_request.json = {"username": "dadams", "password": 42} # UserDict is dict-like in all meaningful ways, but not a subclass of `dict` # it will therefore need to be converted when used argmap = collections.UserDict( {"username": fields.String(), "password": fields.Field()} ) ret = parser.parse(argmap, web_request) assert ret == {"username": "dadams", "password": 42} def test_use_args_allows_non_dict_argmap_mapping(parser, web_request): web_request.json = {"username": "dadams", "password": 42} # UserDict is dict-like in all meaningful ways, but not a subclass of `dict` # it will therefore need to be converted when used argmap = collections.UserDict( {"username": fields.String(), "password": fields.Field()} ) @parser.use_args(argmap, web_request) def viewfunc(args): return args assert viewfunc() == {"username": "dadams", "password": 42} def test_parse_rejects_unknown_argmap_type(parser, web_request): web_request.json = {"username": "dadams", "password": 42} class MyType: pass with pytest.raises(TypeError, match="argmap was of unexpected type"): parser.parse(MyType(), web_request) def test_parser_opt_out_positional_args(web_request): class OptOutParser(MockRequestParser): USE_ARGS_POSITIONAL = False parser = MockRequestParser() opt_out_parser = OptOutParser() web_request.json = {"foo": "bar"} # first, test the behavior of a base parser for comparison # # no specific arg name, default parser, everything works # works for 'args', 'json_args', or any other name @parser.use_args({"foo": fields.Field()}, web_request) def viewfunc1(args): return args @parser.use_args({"foo": fields.Field()}, web_request) def viewfunc2(json_args): return json_args assert viewfunc1() == {"foo": "bar"} assert viewfunc2() == {"foo": "bar"} # second, test the behavior of a parser which sets USE_ARGS_POSITIONAL=False # # `json_args` as the arg name works as a positional or keyword-only # but `args` as the arg name does not @opt_out_parser.use_args({"foo": fields.Field()}, web_request) def viewfunc3(json_args): return json_args @opt_out_parser.use_args({"foo": fields.Field()}, web_request) def viewfunc4(*, json_args): return json_args @opt_out_parser.use_args({"foo": fields.Field()}, web_request) def viewfunc5(args): return args assert viewfunc3() == {"foo": "bar"} assert viewfunc4() == {"foo": "bar"} with pytest.raises(TypeError): assert viewfunc5() def test_use_args_implicit_arg_names(web_request): class OptOutParser(MockRequestParser): USE_ARGS_POSITIONAL = False parser = OptOutParser() web_request.json = {"foo": "bar"} web_request.query = {"bar": "baz"} @parser.use_args({"foo": fields.Field()}, web_request) @parser.use_args({"bar": fields.Field()}, web_request, location="query") def viewfunc(*, json_args, query_args): return (json_args, query_args) assert viewfunc() == ({"foo": "bar"}, {"bar": "baz"}) @pytest.mark.parametrize("use_positional_setting", (True, False)) def test_use_args_explicit_arg_names(web_request, use_positional_setting): class MyParser(MockRequestParser): USE_ARGS_POSITIONAL = use_positional_setting parser = MyParser() web_request.json = {"foo": "bar"} web_request.query = {"bar": "baz"} @parser.use_args({"foo": fields.Field()}, web_request, arg_name="j") @parser.use_args( {"bar": fields.Field()}, web_request, location="query", arg_name="q" ) def viewfunc(*, j, q): return (j, q) assert viewfunc() == ({"foo": "bar"}, {"bar": "baz"}) def test_use_args_errors_on_explicit_arg_name_conflict(web_request): parser = MockRequestParser() web_request.json = {"foo": "bar"} web_request.query = {"bar": "baz"} with pytest.raises(ValueError, match="Attempted to pass `arg_name='q'`"): @parser.use_args({"foo": fields.Field()}, web_request, arg_name="q") @parser.use_args( {"bar": fields.Field()}, web_request, location="query", arg_name="q" ) def viewfunc(*, j, q): return (j, q) def test_use_args_errors_on_implicit_arg_name_conflict(web_request): class MyParser(MockRequestParser): USE_ARGS_POSITIONAL = False parser = MyParser() web_request.json = {"foo": "bar"} with pytest.raises(ValueError, match="Attempted to pass `arg_name='json_args'`"): @parser.use_args({"foo": fields.Field()}, web_request) @parser.use_args({"foo": fields.Field()}, web_request) def viewfunc(*, j, q): return (j, q) def test_use_args_with_arg_name_supports_multi_stacked_decorators(web_request): # this test case specifically explores the use-case in which a view function is # decorated with one `use_args` call, and then "permuted" by decorating it with # other `use_args` calls class MyParser(MockRequestParser): USE_ARGS_POSITIONAL = False parser = MyParser() # create two body variants of the same route, from a single function # they share the same query params with_body_foo = parser.use_args({"foo": fields.Field()}, web_request) with_body_bar = parser.use_args({"bar": fields.Field()}, web_request) @parser.use_args({"snork": fields.Field()}, web_request, location="query") def mypartial(*, json_args, query_args): return (json_args, query_args) route_foo = with_body_foo(mypartial) route_bar = with_body_bar(mypartial) # first, test that these behave as expected web_request.json = {"foo": "bar"} web_request.query = {"snork": 2} assert route_foo() == ({"foo": "bar"}, {"snork": 2}) web_request.json = {"bar": "baz"} assert route_bar() == ({"bar": "baz"}, {"snork": 2}) # now, inspect their internal state assert mypartial.__webargs_argnames__ == ("query_args",) assert route_foo.__webargs_argnames__ == ("query_args", "json_args") assert route_bar.__webargs_argnames__ == ("query_args", "json_args") def test_default_arg_name_pattern_is_customizable(web_request): class MyParser(MockRequestParser): USE_ARGS_POSITIONAL = False def get_default_arg_name(self, location, schema): if location == "json": return "body" elif location == "query": return "query" else: return super().get_default_arg_name(location, schema) parser = MyParser() @parser.use_args({"frob": fields.Field()}, web_request, location="json") @parser.use_args({"snork": fields.Field()}, web_request, location="query") def myview(*, body, query): return (body, query) web_request.json = {"frob": "demuddler"} web_request.query = {"snork": 2} assert myview() == ({"frob": "demuddler"}, {"snork": 2}) def test_default_arg_name_pattern_still_allows_conflict_detection(): class MyParser(MockRequestParser): USE_ARGS_POSITIONAL = False def get_default_arg_name(self, location, schema): return "data" parser = MyParser() with pytest.raises(ValueError, match="Attempted to pass `arg_name='data'`"): @parser.use_args({"frob": fields.Field()}, web_request, location="json") @parser.use_args({"snork": fields.Field()}, web_request, location="query") def myview(*, body, query): return (body, query) def test_parse_with_dict_passes_schema_to_argname_derivation(web_request): default_argname_was_called = False class MyParser(MockRequestParser): USE_ARGS_POSITIONAL = False def get_default_arg_name(self, location, schema): assert isinstance(schema, Schema) nonlocal default_argname_was_called default_argname_was_called = True return super().get_default_arg_name(location, schema) parser = MyParser() @parser.use_args({"foo": fields.Field()}, web_request, location="json") def myview(*, json_args): return json_args web_request.json = {"foo": 42} assert myview() == {"foo": 42} assert default_argname_was_called def test_default_arg_name_pattern_can_pull_schema_attribute(web_request): # this test matches a documentation example exactly class RectangleSchema(Schema): _webargs_arg_name = "rectangle" length = fields.Integer() width = fields.Integer() class MyParser(MockRequestParser): USE_ARGS_POSITIONAL = False def get_default_arg_name(self, location, schema): assert schema is not None if hasattr(schema, "_webargs_arg_name"): if isinstance(schema._webargs_arg_name, str): return schema._webargs_arg_name return super().get_default_arg_name(location, schema) parser = MyParser() @parser.use_args(RectangleSchema, web_request, location="json") def area(*, rectangle): return rectangle["length"] * rectangle["width"] web_request.json = {"length": 6, "width": 7} assert area() == 42 webargs-8.5.0/tests/test_djangoparser.py000066400000000000000000000031541461250545000204150ustar00rootroot00000000000000import pytest from tests.apps.django_app import DJANGO_SUPPORTS_ASYNC from tests.apps.django_app.base.wsgi import application from webargs.testing import CommonTestCase class TestDjangoParser(CommonTestCase): def create_app(self): return application @pytest.mark.skip( reason="skipping because DjangoParser does not implement handle_error" ) def test_use_args_with_validation(self): pass def test_parsing_in_class_based_view(self, testapp): assert testapp.get("/echo_cbv?name=Fred").json == {"name": "Fred"} assert testapp.post_json("/echo_cbv", {"name": "Fred"}).json == {"name": "Fred"} def test_use_args_in_class_based_view(self, testapp): res = testapp.get("/echo_use_args_cbv?name=Fred") assert res.json == {"name": "Fred"} res = testapp.post_json("/echo_use_args_cbv", {"name": "Fred"}) assert res.json == {"name": "Fred"} def test_use_args_in_class_based_view_with_path_param(self, testapp): res = testapp.get("/echo_use_args_with_path_param_cbv/42?name=Fred") assert res.json == {"name": "Fred"} @pytest.mark.skipif( not DJANGO_SUPPORTS_ASYNC, reason="requires a django version with async support" ) def test_parse_querystring_args_async(self, testapp): assert testapp.get("/async_echo?name=Fred").json == {"name": "Fred"} @pytest.mark.skipif( not DJANGO_SUPPORTS_ASYNC, reason="requires a django version with async support" ) def test_async_use_args_decorator(self, testapp): assert testapp.get("/async_echo_use_args?name=Fred").json == {"name": "Fred"} webargs-8.5.0/tests/test_falconparser.py000066400000000000000000000065421461250545000204210ustar00rootroot00000000000000import falcon.testing import pytest from tests.apps.falcon_app import FALCON_SUPPORTS_ASYNC, create_app, create_async_app from webargs.testing import CommonTestCase class TestFalconParser(CommonTestCase): def create_app(self): return create_app() @pytest.mark.skip(reason="files location not supported for falconparser") def test_parse_files(self, testapp): pass def test_use_args_hook(self, testapp): assert testapp.get("/echo_use_args_hook?name=Fred").json == {"name": "Fred"} def test_parse_media(self, testapp): assert testapp.post_json("/echo_media", {"name": "Fred"}).json == { "name": "Fred" } def test_parse_media_missing(self, testapp): assert testapp.post("/echo_media", "").json == {"name": "World"} def test_parse_media_empty(self, testapp): assert testapp.post_json("/echo_media", {}).json == {"name": "World"} def test_parse_media_error_unexpected_int(self, testapp): res = testapp.post_json("/echo_media", 1, expect_errors=True) assert res.status_code == 422 # https://github.com/marshmallow-code/webargs/issues/427 @pytest.mark.parametrize("path", ["/echo_json", "/echo_media"]) def test_parse_json_with_nonutf8_chars(self, testapp, path): res = testapp.post( path, b"\xfe", headers={"Accept": "application/json", "Content-Type": "application/json"}, expect_errors=True, ) assert res.status_code == 400 if path.endswith("json"): assert res.json["errors"] == {"json": ["Invalid JSON body."]} # https://github.com/sloria/webargs/issues/329 @pytest.mark.parametrize("path", ["/echo_json", "/echo_media"]) def test_invalid_json(self, testapp, path): res = testapp.post( path, '{"foo": "bar", }', headers={"Accept": "application/json", "Content-Type": "application/json"}, expect_errors=True, ) assert res.status_code == 400 if path.endswith("json"): assert res.json["errors"] == {"json": ["Invalid JSON body."]} # Falcon converts headers to all-caps def test_parsing_headers(self, testapp): res = testapp.get("/echo_headers", headers={"name": "Fred"}) assert res.json == {"NAME": "Fred"} # `falcon.testing.TestClient.simulate_request` parses request with `wsgiref` def test_body_parsing_works_with_simulate(self): app = self.create_app() client = falcon.testing.TestClient(app) res = client.simulate_post( "/echo_json", json={"name": "Fred"}, ) assert res.json == {"name": "Fred"} @pytest.mark.skipif( not FALCON_SUPPORTS_ASYNC, reason="requires a falcon version with async support" ) def test_parse_querystring_args_async(self): app = create_async_app() client = falcon.testing.TestClient(app) assert client.simulate_get("/async_echo?name=Fred").json == {"name": "Fred"} @pytest.mark.skipif( not FALCON_SUPPORTS_ASYNC, reason="requires a falcon version with async support" ) def test_async_use_args_decorator(self): app = create_async_app() client = falcon.testing.TestClient(app) assert client.simulate_get("/async_echo_use_args?name=Fred").json == { "name": "Fred" } webargs-8.5.0/tests/test_flaskparser.py000066400000000000000000000213561461250545000202570ustar00rootroot00000000000000from unittest import mock import pytest from flask import Flask from marshmallow import Schema from werkzeug.exceptions import BadRequest, HTTPException from webargs import ValidationError, fields, missing from webargs.core import json from webargs.flaskparser import abort, parser from webargs.testing import CommonTestCase from .apps.flask_app import FLASK_SUPPORTS_ASYNC, app class TestFlaskParser(CommonTestCase): def create_app(self): return app def test_parsing_view_args(self, testapp): res = testapp.get("/echo_view_arg/42") assert res.json == {"view_arg": 42} def test_parsing_invalid_view_arg(self, testapp): res = testapp.get("/echo_view_arg/foo", expect_errors=True) assert res.status_code == 422 assert res.json == {"view_args": {"view_arg": ["Not a valid integer."]}} def test_use_args_with_view_args_parsing(self, testapp): res = testapp.get("/echo_view_arg_use_args/42") assert res.json == {"view_arg": 42} def test_use_args_on_a_method_view(self, testapp): res = testapp.post_json("/echo_method_view_use_args", {"val": 42}) assert res.json == {"val": 42} def test_use_kwargs_on_a_method_view(self, testapp): res = testapp.post_json("/echo_method_view_use_kwargs", {"val": 42}) assert res.json == {"val": 42} def test_use_kwargs_with_missing_data(self, testapp): res = testapp.post_json("/echo_use_kwargs_missing", {"username": "foo"}) assert res.json == {"username": "foo"} # regression test for https://github.com/marshmallow-code/webargs/issues/145 def test_nested_many_with_data_key(self, testapp): post_with_raw_fieldname_args = ( "/echo_nested_many_data_key", {"x_field": [{"id": 42}]}, ) res = testapp.post_json(*post_with_raw_fieldname_args, expect_errors=True) assert res.status_code == 422 res = testapp.post_json("/echo_nested_many_data_key", {"X-Field": [{"id": 24}]}) assert res.json == {"x_field": [{"id": 24}]} res = testapp.post_json("/echo_nested_many_data_key", {}) assert res.json == {} # regression test for https://github.com/marshmallow-code/webargs/issues/500 def test_parsing_unexpected_headers_when_raising(self, testapp): res = testapp.get( "/echo_headers_raising", expect_errors=True, headers={"X-Unexpected": "foo"} ) assert res.status_code == 422 assert "headers" in res.json assert "X-Unexpected" in set(res.json["headers"].keys()) class TestFlaskAsyncParser(CommonTestCase): def create_app(self): return app @pytest.mark.skipif( not FLASK_SUPPORTS_ASYNC, reason="requires async support in flask" ) def test_parsing_view_args_async(self, testapp): res = testapp.get("/echo_view_arg_async/42") assert res.json == {"view_arg": 42} @pytest.mark.skipif( not FLASK_SUPPORTS_ASYNC, reason="requires async support in flask" ) def test_parsing_invalid_view_arg_async(self, testapp): res = testapp.get("/echo_view_arg_async/foo", expect_errors=True) assert res.status_code == 422 assert res.json == {"view_args": {"view_arg": ["Not a valid integer."]}} @pytest.mark.skipif( not FLASK_SUPPORTS_ASYNC, reason="requires async support in flask" ) def test_use_args_with_view_args_parsing_async(self, testapp): res = testapp.get("/echo_view_arg_use_args_async/42") assert res.json == {"view_arg": 42} @pytest.mark.skipif( not FLASK_SUPPORTS_ASYNC, reason="requires async support in flask" ) def test_use_args_on_a_method_view_async(self, testapp): res = testapp.post_json("/echo_method_view_use_args_async", {"val": 42}) assert res.json == {"val": 42} @pytest.mark.skipif( not FLASK_SUPPORTS_ASYNC, reason="requires async support in flask" ) def test_use_kwargs_on_a_method_view_async(self, testapp): res = testapp.post_json("/echo_method_view_use_kwargs_async", {"val": 42}) assert res.json == {"val": 42} @pytest.mark.skipif( not FLASK_SUPPORTS_ASYNC, reason="requires async support in flask" ) def test_use_kwargs_with_missing_data_async(self, testapp): res = testapp.post_json("/echo_use_kwargs_missing_async", {"username": "foo"}) assert res.json == {"username": "foo"} # regression test for https://github.com/marshmallow-code/webargs/issues/145 @pytest.mark.skipif( not FLASK_SUPPORTS_ASYNC, reason="requires async support in flask" ) def test_nested_many_with_data_key_async(self, testapp): post_with_raw_fieldname_args = ( "/echo_nested_many_data_key_async", {"x_field": [{"id": 42}]}, ) res = testapp.post_json(*post_with_raw_fieldname_args, expect_errors=True) assert res.status_code == 422 res = testapp.post_json( "/echo_nested_many_data_key_async", {"X-Field": [{"id": 24}]} ) assert res.json == {"x_field": [{"id": 24}]} res = testapp.post_json("/echo_nested_many_data_key_async", {}) assert res.json == {} # regression test for https://github.com/marshmallow-code/webargs/issues/500 @pytest.mark.skipif( not FLASK_SUPPORTS_ASYNC, reason="requires async support in flask" ) def test_parsing_unexpected_headers_when_raising_async(self, testapp): res = testapp.get( "/echo_headers_raising_async", expect_errors=True, headers={"X-Unexpected": "foo"}, ) assert res.status_code == 422 assert "headers" in res.json assert "X-Unexpected" in set(res.json["headers"].keys()) @mock.patch("webargs.flaskparser.abort") def test_abort_called_on_validation_error(mock_abort): # error handling must raise an error to be valid mock_abort.side_effect = BadRequest("foo") app = Flask("testapp") def validate(x): return x == 42 argmap = {"value": fields.Field(validate=validate)} with app.test_request_context( "/foo", method="post", data=json.dumps({"value": 41}), content_type="application/json", ): with pytest.raises(HTTPException): parser.parse(argmap) mock_abort.assert_called() abort_args, abort_kwargs = mock_abort.call_args assert abort_args[0] == 422 expected_msg = "Invalid value." assert abort_kwargs["messages"]["json"]["value"] == [expected_msg] assert type(abort_kwargs["exc"]) is ValidationError @pytest.mark.asyncio @pytest.mark.skipif(not FLASK_SUPPORTS_ASYNC, reason="requires async support in flask") async def test_abort_called_on_validation_error_async(): with mock.patch("webargs.flaskparser.abort") as mock_abort: # error handling must raise an error to be valid mock_abort.side_effect = BadRequest("foo") app = Flask("testapp") def validate(x): return x == 42 argmap = {"value": fields.Field(validate=validate)} with app.test_request_context( "/foo", method="post", data=json.dumps({"value": 41}), content_type="application/json", ): with pytest.raises(HTTPException): await parser.async_parse(argmap) mock_abort.assert_called() abort_args, abort_kwargs = mock_abort.call_args assert abort_args[0] == 422 expected_msg = "Invalid value." assert abort_kwargs["messages"]["json"]["value"] == [expected_msg] assert type(abort_kwargs["exc"]) is ValidationError @pytest.mark.parametrize("mimetype", [None, "application/json"]) def test_load_json_returns_missing_if_no_data(mimetype): req = mock.Mock() req.mimetype = mimetype req.get_data.return_value = "" schema = Schema.from_dict({"foo": fields.Field()})() assert parser.load_json(req, schema) is missing def test_abort_with_message(): with pytest.raises(HTTPException) as excinfo: abort(400, message="custom error message") assert excinfo.value.data["message"] == "custom error message" def test_abort_has_serializable_data(): with pytest.raises(HTTPException) as excinfo: abort(400, message="custom error message") serialized_error = json.dumps(excinfo.value.data) error = json.loads(serialized_error) assert isinstance(error, dict) assert error["message"] == "custom error message" with pytest.raises(HTTPException) as excinfo: abort( 400, message="custom error message", exc=ValidationError("custom error message"), ) serialized_error = json.dumps(excinfo.value.data) error = json.loads(serialized_error) assert isinstance(error, dict) assert error["message"] == "custom error message" webargs-8.5.0/tests/test_pyramidparser.py000066400000000000000000000006731461250545000206230ustar00rootroot00000000000000from webargs.testing import CommonTestCase class TestPyramidParser(CommonTestCase): def create_app(self): from .apps.pyramid_app import create_app return create_app() def test_use_args_with_callable_view(self, testapp): assert testapp.get("/echo_callable?value=42").json == {"value": 42} def test_parse_matchdict(self, testapp): assert testapp.get("/echo_matchdict/42").json == {"mymatch": 42} webargs-8.5.0/tests/test_tornadoparser.py000066400000000000000000000432121461250545000206200ustar00rootroot00000000000000from unittest import mock from urllib.parse import urlencode import marshmallow as ma import pytest import tornado.concurrent import tornado.http1connection import tornado.httpserver import tornado.httputil import tornado.ioloop import tornado.web from tornado.testing import AsyncHTTPTestCase from webargs import fields, missing from webargs.core import json, parse_json from webargs.tornadoparser import ( WebArgsTornadoMultiDictProxy, parser, use_args, use_kwargs, ) name = "name" value = "value" class AuthorSchema(ma.Schema): name = fields.Str(load_default="World", validate=lambda n: len(n) >= 3) works = fields.List(fields.Str()) author_schema = AuthorSchema() def test_tornado_multidictproxy(): for dictval, fieldname, expected in ( ({"name": "Sophocles"}, "name", "Sophocles"), ({"name": "Sophocles"}, "works", missing), ({"works": ["Antigone", "Oedipus Rex"]}, "works", ["Antigone", "Oedipus Rex"]), ({"works": ["Antigone", "Oedipus at Colonus"]}, "name", missing), ): proxy = WebArgsTornadoMultiDictProxy(dictval, author_schema) assert proxy.get(fieldname) == expected class TestQueryArgs: def test_it_should_get_single_values(self): query = [("name", "Aeschylus")] request = make_get_request(query) result = parser.load_querystring(request, author_schema) assert result["name"] == "Aeschylus" def test_it_should_get_multiple_values(self): query = [("works", "Agamemnon"), ("works", "Nereids")] request = make_get_request(query) result = parser.load_querystring(request, author_schema) assert result["works"] == ["Agamemnon", "Nereids"] def test_it_should_return_missing_if_not_present(self): query = [] request = make_get_request(query) result = parser.load_querystring(request, author_schema) assert result["name"] is missing assert result["works"] is missing class TestFormArgs: def test_it_should_get_single_values(self): query = [("name", "Aristophanes")] request = make_form_request(query) result = parser.load_form(request, author_schema) assert result["name"] == "Aristophanes" def test_it_should_get_multiple_values(self): query = [("works", "The Wasps"), ("works", "The Frogs")] request = make_form_request(query) result = parser.load_form(request, author_schema) assert result["works"] == ["The Wasps", "The Frogs"] def test_it_should_return_missing_if_not_present(self): query = [] request = make_form_request(query) result = parser.load_form(request, author_schema) assert result["name"] is missing assert result["works"] is missing class TestJSONArgs: def test_it_should_get_single_values(self): query = {"name": "Euripides"} request = make_json_request(query) result = parser.load_json(request, author_schema) assert result["name"] == "Euripides" def test_parsing_request_with_vendor_content_type(self): query = {"name": "Euripides"} request = make_json_request( query, content_type="application/vnd.api+json; charset=UTF-8" ) result = parser.load_json(request, author_schema) assert result["name"] == "Euripides" def test_it_should_get_multiple_values(self): query = {"works": ["Medea", "Electra"]} request = make_json_request(query) result = parser.load_json(request, author_schema) assert result["works"] == ["Medea", "Electra"] def test_it_should_get_multiple_nested_values(self): class CustomSchema(ma.Schema): works = fields.List( fields.Nested({"author": fields.Str(), "workname": fields.Str()}) ) custom_schema = CustomSchema() query = { "works": [ {"author": "Euripides", "workname": "Hecuba"}, {"author": "Aristophanes", "workname": "The Birds"}, ] } request = make_json_request(query) result = parser.load_json(request, custom_schema) assert result["works"] == [ {"author": "Euripides", "workname": "Hecuba"}, {"author": "Aristophanes", "workname": "The Birds"}, ] def test_it_should_not_include_fieldnames_if_not_present(self): query = {} request = make_json_request(query) result = parser.load_json(request, author_schema) assert result == {} @pytest.mark.usefixtures("event_loop") def test_it_should_handle_type_error_on_load_json(self): # but this is different from the test above where the payload was valid # and empty -- missing vs {} # NOTE: `event_loop` is the pytest-aiohttp event loop fixture, but it's # important to get an event loop here so that we can construct a future request = make_request( body=tornado.concurrent.Future(), headers={"Content-Type": "application/json"}, ) result = parser.load_json(request, author_schema) assert result is missing def test_it_should_handle_value_error_on_parse_json(self): request = make_request("this is json not") result = parser.load_json(request, author_schema) assert result is missing class TestHeadersArgs: def test_it_should_get_single_values(self): query = {"name": "Euphorion"} request = make_request(headers=query) result = parser.load_headers(request, author_schema) assert result["name"] == "Euphorion" def test_it_should_get_multiple_values(self): query = {"works": ["Prometheus Bound", "Prometheus Unbound"]} request = make_request(headers=query) result = parser.load_headers(request, author_schema) assert result["works"] == ["Prometheus Bound", "Prometheus Unbound"] def test_it_should_return_missing_if_not_present(self): request = make_request() result = parser.load_headers(request, author_schema) assert result["name"] is missing assert result["works"] is missing class TestFilesArgs: def test_it_should_get_single_values(self): query = [("name", "Sappho")] request = make_files_request(query) result = parser.load_files(request, author_schema) assert result["name"] == "Sappho" def test_it_should_get_multiple_values(self): query = [("works", "Sappho 31"), ("works", "Ode to Aphrodite")] request = make_files_request(query) result = parser.load_files(request, author_schema) assert result["works"] == ["Sappho 31", "Ode to Aphrodite"] def test_it_should_return_missing_if_not_present(self): query = [] request = make_files_request(query) result = parser.load_files(request, author_schema) assert result["name"] is missing assert result["works"] is missing class TestErrorHandler: def test_it_should_raise_httperror_on_failed_validation(self): args = {"foo": fields.Field(validate=lambda x: False)} with pytest.raises(tornado.web.HTTPError): parser.parse(args, make_json_request({"foo": 42})) class TestParse: def test_it_should_parse_query_arguments(self): attrs = {"string": fields.Field(), "integer": fields.List(fields.Int())} request = make_get_request( [("string", "value"), ("integer", "1"), ("integer", "2")] ) parsed = parser.parse(attrs, request, location="query") assert parsed["integer"] == [1, 2] assert parsed["string"] == value def test_it_should_parse_form_arguments(self): attrs = {"string": fields.Field(), "integer": fields.List(fields.Int())} request = make_form_request( [("string", "value"), ("integer", "1"), ("integer", "2")] ) parsed = parser.parse(attrs, request, location="form") assert parsed["integer"] == [1, 2] assert parsed["string"] == value def test_it_should_parse_json_arguments(self): attrs = {"string": fields.Str(), "integer": fields.List(fields.Int())} request = make_json_request({"string": "value", "integer": [1, 2]}) parsed = parser.parse(attrs, request) assert parsed["integer"] == [1, 2] assert parsed["string"] == value def test_it_should_raise_when_json_is_invalid(self): attrs = {"foo": fields.Str()} request = make_request( body='{"foo": 42,}', headers={"Content-Type": "application/json"} ) with pytest.raises(tornado.web.HTTPError) as excinfo: parser.parse(attrs, request) error = excinfo.value assert error.status_code == 400 assert error.messages == {"json": ["Invalid JSON body."]} def test_it_should_parse_header_arguments(self): attrs = {"string": fields.Str(), "integer": fields.List(fields.Int())} request = make_request(headers={"string": "value", "integer": ["1", "2"]}) parsed = parser.parse(attrs, request, location="headers") assert parsed["string"] == value assert parsed["integer"] == [1, 2] def test_it_should_parse_cookies_arguments(self): attrs = {"string": fields.Str(), "integer": fields.List(fields.Int())} request = make_cookie_request( [("string", "value"), ("integer", "1"), ("integer", "2")] ) parsed = parser.parse(attrs, request, location="cookies") assert parsed["string"] == value assert parsed["integer"] == [2] def test_it_should_parse_files_arguments(self): attrs = {"string": fields.Str(), "integer": fields.List(fields.Int())} request = make_files_request( [("string", "value"), ("integer", "1"), ("integer", "2")] ) parsed = parser.parse(attrs, request, location="files") assert parsed["string"] == value assert parsed["integer"] == [1, 2] def test_it_should_parse_required_arguments(self): args = {"foo": fields.Field(required=True)} request = make_json_request({}) msg = "Missing data for required field." with pytest.raises(tornado.web.HTTPError, match=msg): parser.parse(args, request) def test_it_should_parse_multiple_arg_required(self): args = {"foo": fields.List(fields.Int(), required=True)} request = make_json_request({}) msg = "Missing data for required field." with pytest.raises(tornado.web.HTTPError, match=msg): parser.parse(args, request) class TestUseArgs: def test_it_should_pass_parsed_as_first_argument(self): class Handler: request = make_json_request({"key": "value"}) @use_args({"key": fields.Field()}) def get(self, *args, **kwargs): assert args[0] == {"key": "value"} assert kwargs == {} return True handler = Handler() result = handler.get() assert result is True def test_it_should_pass_parsed_as_kwargs_arguments(self): class Handler: request = make_json_request({"key": "value"}) @use_kwargs({"key": fields.Field()}) def get(self, *args, **kwargs): assert args == () assert kwargs == {"key": "value"} return True handler = Handler() result = handler.get() assert result is True def test_it_should_be_validate_arguments_when_validator_is_passed(self): class Handler: request = make_json_request({"foo": 41}) @use_kwargs({"foo": fields.Int()}, validate=lambda args: args["foo"] > 42) def get(self, args): return True handler = Handler() with pytest.raises(tornado.web.HTTPError): handler.get() def make_uri(args): return "/test?" + urlencode(args) def make_form_body(args): return urlencode(args) def make_json_body(args): return json.dumps(args) def make_get_request(args): return make_request(uri=make_uri(args)) def make_form_request(args): return make_request( body=make_form_body(args), headers={"Content-Type": "application/x-www-form-urlencoded"}, ) def make_json_request(args, content_type="application/json; charset=UTF-8"): return make_request( body=make_json_body(args), headers={"Content-Type": content_type} ) def make_cookie_request(args): return make_request(headers={"Cookie": " ;".join("=".join(pair) for pair in args)}) def make_files_request(args): files = {} for key, value in args: if isinstance(value, list): files.setdefault(key, []).extend(value) else: files.setdefault(key, []).append(value) return make_request(files=files) def make_request(uri=None, body=None, headers=None, files=None): uri = uri if uri is not None else "" body = body if body is not None else "" method = "POST" if body else "GET" # Need to make a mock connection right now because Tornado 4.0 requires a # remote_ip in the context attribute. 4.1 addresses this, and this # will be unnecessary once it is released # https://github.com/tornadoweb/tornado/issues/1118 mock_connection = mock.Mock(spec=tornado.http1connection.HTTP1Connection) mock_connection.context = mock.Mock() mock_connection.remote_ip = None content_type = headers.get("Content-Type", "") if headers else "" request = tornado.httputil.HTTPServerRequest( method=method, uri=uri, body=body, headers=headers, files=files, connection=mock_connection, ) tornado.httputil.parse_body_arguments( content_type=content_type, body=body.encode("latin-1") if hasattr(body, "encode") else body, arguments=request.body_arguments, files=request.files, ) return request class EchoHandler(tornado.web.RequestHandler): ARGS = {"name": fields.Str()} @use_args(ARGS, location="query") def get(self, args): self.write(args) class EchoFormHandler(tornado.web.RequestHandler): ARGS = {"name": fields.Str()} @use_args(ARGS, location="form") def post(self, args): self.write(args) class EchoJSONHandler(tornado.web.RequestHandler): ARGS = {"name": fields.Str()} @use_args(ARGS) def post(self, args): self.write(args) class EchoWithParamHandler(tornado.web.RequestHandler): ARGS = {"name": fields.Str()} @use_args(ARGS, location="query") def get(self, id, args): self.write(args) echo_app = tornado.web.Application( [ (r"/echo", EchoHandler), (r"/echo_form", EchoFormHandler), (r"/echo_json", EchoJSONHandler), (r"/echo_with_param/(\d+)", EchoWithParamHandler), ] ) class TestApp(AsyncHTTPTestCase): def get_app(self): return echo_app def test_post(self): res = self.fetch( "/echo_json", method="POST", headers={"Content-Type": "application/json"}, body=json.dumps({"name": "Steve"}), ) json_body = parse_json(res.body) assert json_body["name"] == "Steve" res = self.fetch( "/echo_json", method="POST", headers={"Content-Type": "application/json"}, body=json.dumps({}), ) json_body = parse_json(res.body) assert "name" not in json_body def test_get_with_no_json_body(self): res = self.fetch( "/echo", method="GET", headers={"Content-Type": "application/json"} ) json_body = parse_json(res.body) assert "name" not in json_body def test_get_path_param(self): res = self.fetch( "/echo_with_param/42?name=Steve", method="GET", headers={"Content-Type": "application/json"}, ) json_body = parse_json(res.body) assert json_body == {"name": "Steve"} class ValidateHandler(tornado.web.RequestHandler): ARGS = {"name": fields.Str(required=True)} @use_args(ARGS) def post(self, args): self.write(args) @use_kwargs(ARGS, location="query") def get(self, name): self.write({"status": "success"}) def always_fail(val): raise ma.ValidationError("something went wrong") class AlwaysFailHandler(tornado.web.RequestHandler): ARGS = {"name": fields.Str(validate=always_fail)} @use_args(ARGS) def post(self, args): self.write(args) validate_app = tornado.web.Application( [(r"/echo", ValidateHandler), (r"/alwaysfail", AlwaysFailHandler)] ) class TestValidateApp(AsyncHTTPTestCase): def get_app(self): return validate_app def test_required_field_provided(self): res = self.fetch( "/echo", method="POST", headers={"Content-Type": "application/json"}, body=json.dumps({"name": "johnny"}), ) json_body = parse_json(res.body) assert json_body["name"] == "johnny" def test_missing_required_field_throws_422(self): res = self.fetch( "/echo", method="POST", headers={"Content-Type": "application/json"}, body=json.dumps({"occupation": "pizza"}), ) assert res.code == 422 def test_user_validator_returns_422_by_default(self): res = self.fetch( "/alwaysfail", method="POST", headers={"Content-Type": "application/json"}, body=json.dumps({"name": "Steve"}), ) assert res.code == 422 def test_use_kwargs_with_error(self): res = self.fetch("/echo", method="GET") assert res.code == 422 if __name__ == "__main__": echo_app.listen(8888) tornado.ioloop.IOLoop.instance().start() webargs-8.5.0/tox.ini000066400000000000000000000036021461250545000144740ustar00rootroot00000000000000[tox] envlist= lint py{38,39,310,311,312}-marshmallow3 py312-marshmallowdev py38-lowest docs [testenv] extras = tests deps = marshmallow3: marshmallow>=3.0.0,<4.0.0 marshmallowdev: https://github.com/marshmallow-code/marshmallow/archive/dev.tar.gz # for 'mindeps', pin flask to 1.1.3 and markupsafe to 1.1.1 # because flask 1.x does not set upper bounds on the versions of its dependencies # generally, we can't install just any version from 1.x -- only 1.1.3 and 1.1.4 # markupsafe is a second-order dependency of flask (flask -> jinja2 -> markupsafe) # and must be pinned explicitly because jinja2 does not pin its dependencies in any # versions in its 2.x line # see: # https://github.com/marshmallow-code/webargs/pull/694 # https://github.com/pallets/flask/pull/4047 # https://github.com/pallets/flask/issues/4455 lowest: flask==1.1.3 lowest: markupsafe==1.1.1 # all non-flask frameworks lowest: Django==2.2.0 lowest: bottle==0.12.13 lowest: tornado==4.5.2 lowest: pyramid==1.9.1 lowest: falcon==2.0.0 lowest: aiohttp==3.0.8 commands = pytest {posargs} [testenv:lint] deps = pre-commit~=3.5 skip_install = true commands = pre-commit run --all-files # a separate `mypy` target which runs `mypy` in an environment with # `webargs` and `marshmallow` both installed is a valuable safeguard against # issues in which `mypy` running on every file standalone won't catch things [testenv:mypy] deps = mypy==1.8.0 extras = frameworks commands = mypy src/ {posargs} [testenv:docs] extras = docs commands = sphinx-build docs/ docs/_build {posargs} ; Below tasks are for development only (not run in CI) [testenv:watch-docs] deps = sphinx-autobuild extras = docs commands = sphinx-autobuild --open-browser docs/ docs/_build {posargs} --watch src/webargs --delay 2 [testenv:watch-readme] deps = restview skip_install = true commands = restview README.rst