From 3c8142d812017b817a7edf370b2db66efab15a86 Mon Sep 17 00:00:00 2001 From: Mathieu Leplatre Date: Tue, 30 Jan 2024 19:40:23 +0100 Subject: [PATCH 1/9] Modernize repository --- .coveragerc | 2 - .github/dependabot.yml | 19 ++-- .github/release.yml | 23 +++++ .github/workflows/labels.yaml | 14 +++ .github/workflows/publish.yml | 55 ++++++++++++ .github/workflows/test.yml | 63 +++++--------- CHANGES.txt | 6 +- MANIFEST.in | 3 - Makefile | 53 ++++++----- pyproject.toml | 73 ++++++++++++++++ requirements.in | 1 + requirements.txt | 87 +++++++++++++++++++ setup.cfg | 2 - setup.py | 44 ---------- {cornice => src/cornice}/__init__.py | 0 {cornice => src/cornice}/cors.py | 0 {cornice => src/cornice}/errors.py | 0 {cornice => src/cornice}/pyramidhook.py | 0 {cornice => src/cornice}/renderer.py | 0 {cornice => src/cornice}/resource.py | 0 {cornice => src/cornice}/service.py | 0 {cornice => src/cornice}/util.py | 0 .../cornice}/validators/__init__.py | 0 .../cornice}/validators/_colander.py | 0 .../cornice}/validators/_marshmallow.py | 0 tests/requirements.txt | 8 -- tox.ini | 30 ------- 27 files changed, 323 insertions(+), 160 deletions(-) delete mode 100644 .coveragerc create mode 100644 .github/release.yml create mode 100644 .github/workflows/labels.yaml create mode 100644 .github/workflows/publish.yml delete mode 100755 MANIFEST.in create mode 100644 pyproject.toml create mode 100644 requirements.in create mode 100644 requirements.txt delete mode 100644 setup.cfg delete mode 100644 setup.py rename {cornice => src/cornice}/__init__.py (100%) rename {cornice => src/cornice}/cors.py (100%) rename {cornice => src/cornice}/errors.py (100%) rename {cornice => src/cornice}/pyramidhook.py (100%) rename {cornice => src/cornice}/renderer.py (100%) rename {cornice => src/cornice}/resource.py (100%) rename {cornice => src/cornice}/service.py (100%) rename {cornice => src/cornice}/util.py (100%) rename {cornice => src/cornice}/validators/__init__.py (100%) rename {cornice => src/cornice}/validators/_colander.py (100%) rename {cornice => src/cornice}/validators/_marshmallow.py (100%) delete mode 100644 tests/requirements.txt delete mode 100644 tox.ini diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 013dd202..00000000 --- a/.coveragerc +++ /dev/null @@ -1,2 +0,0 @@ -[report] -show_missing = True diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b3425aae..332beb04 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,9 +3,16 @@ updates: - package-ecosystem: pip directory: "/" schedule: - interval: daily - open-pull-requests-limit: 10 - ignore: - - dependency-name: marshmallow - versions: - - 3.2.1 + interval: weekly + open-pull-requests-limit: 99 + groups: + all-dependencies: + update-types: ["major", "minor", "patch"] +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 99 + groups: + all-dependencies: + update-types: ["major", "minor", "patch"] diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 00000000..0019e507 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,23 @@ +changelog: + exclude: + authors: + - dependabot + categories: + - title: Breaking Changes + labels: + - "breaking-change" + - title: Bug Fixes + labels: + - "bug" + - title: New Features + labels: + - "enhancement" + - title: Documentation + labels: + - "documentation" + - title: Dependency Updates + labels: + - "dependencies" + - title: Other Changes + labels: + - "*" diff --git a/.github/workflows/labels.yaml b/.github/workflows/labels.yaml new file mode 100644 index 00000000..96a4efb8 --- /dev/null +++ b/.github/workflows/labels.yaml @@ -0,0 +1,14 @@ +name: Force pull-requests label(s) + +on: + pull_request: + types: [opened, labeled, unlabeled] +jobs: + pr-has-label: + name: Will be skipped if labelled + runs-on: ubuntu-latest + if: ${{ join(github.event.pull_request.labels.*.name, ', ') == '' }} + steps: + - run: | + echo 'Pull-request must have at least one label' + exit 1 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..a096adb3 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,55 @@ +name: Publish Python 🐍 distribution πŸ“¦ to PyPI + +on: + push: + tags: + - '*' + +jobs: + build: + name: Build distribution πŸ“¦ + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Print environment + run: | + python --version + + - name: Install pypa/build + run: python3 -m pip install build + + - name: Build a binary wheel and a source tarball + run: python3 -m build + + - name: Store the distribution packages + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + publish-to-pypi: + name: Publish Python 🐍 distribution πŸ“¦ to PyPI + if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes + needs: + - build + runs-on: ubuntu-latest + environment: + name: release + url: https://pypi.org/p/cornice + 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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6c82d030..44f87d61 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,59 +1,38 @@ -on: - push: - branches: - - master - pull_request: +on: pull_request -name: Unit Testing jobs: - chore: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-python@v4 + + - name: Run linting and formatting checks + run: make lint + + unit-tests: name: Unit Tests + needs: lint runs-on: ubuntu-latest strategy: matrix: - toxenv: [py38, py39, py310, py311, flake8] - include: - - toxenv: py38 - python-version: "3.8" - - toxenv: py39 - python-version: "3.9" - - toxenv: py310 - python-version: "3.10" - - toxenv: py311 - python-version: "3.11" - - toxenv: flake8 - python-version: "3.11" + python-version: ["3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - - name: Install virtualenv - run: | - pip install virtualenv - virtualenv --python=python3 .venv - - - name: Print environment - run: | - source .venv/bin/activate - python --version - pip --version + cache: pip - name: Install dependencies - run: | - source .venv/bin/activate - pip install tox + run: make install - - name: Tox - run: | - source .venv/bin/activate - tox -e ${{ matrix.toxenv }} + - name: Run unit tests + run: make test - name: Coveralls - uses: AndreMiras/coveralls-python-action@develop - if: matrix.toxenv != 'flake8' - with: - github-token: ${{ secrets.GITHUB_TOKEN }} + uses: coverallsapp/github-action@v2 diff --git a/CHANGES.txt b/CHANGES.txt index e663ded1..337a2e4f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -2,10 +2,10 @@ CHANGELOG ######### -6.0.2 (unreleased) -================== +>= 6.0.2 +======== -- Add support for Python 3.9 and 3.10. +Since version 6.0.2, we use `Github releases `_ and autogenerated changelogs. 6.0.1 (2022-01-07) diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100755 index 22270aee..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,3 +0,0 @@ -include *.txt -include *.rst -include LICENSE diff --git a/Makefile b/Makefile index 473bf3e3..cfee355a 100644 --- a/Makefile +++ b/Makefile @@ -1,30 +1,43 @@ -HERE = $(shell pwd) -VENV = $(HERE)/.venv -BIN = $(VENV)/bin -PYTHON = $(BIN)/python -VIRTUALENV = virtualenv +VENV := $(shell echo $${VIRTUAL_ENV-.venv}) +PYTHON = $(VENV)/bin/python +INSTALL_STAMP = $(VENV)/.install.stamp -.PHONY: all test docs +.PHONY: all +all: install -all: build +install: $(INSTALL_STAMP) +$(INSTALL_STAMP): $(PYTHON) pyproject.toml requirements.txt + $(VENV)/bin/pip install -U pip + $(VENV)/bin/pip install -r requirements.txt + $(VENV)/bin/pip install -r docs/requirements.txt + $(VENV)/bin/pip install -e ".[dev]" + touch $(INSTALL_STAMP) $(PYTHON): - $(VIRTUALENV) $(VENV) + python3 -m venv $(VENV) -build: $(PYTHON) - $(PYTHON) setup.py develop +requirements.txt: requirements.in + pip-compile -clean: - rm -rf $(VENV) - -test_dependencies: build - $(BIN)/pip install tox +.PHONY: test +test: install + $(VENV)/bin/pytest --cov-report term-missing --cov-fail-under 100 --cov cornice -test: test_dependencies - $(BIN)/tox +.PHONY: lint +lint: install + $(VENV)/bin/ruff check src tests + $(VENV)/bin/ruff format --check src tests -docs_dependencies: $(PYTHON) - $(BIN)/pip install -r docs/requirements.txt +.PHONY: format +format: install + $(VENV)/bin/ruff check --fix src tests + $(VENV)/bin/ruff format src tests -docs: docs_dependencies +docs: install cd docs && $(MAKE) html SPHINXBUILD=$(VENV)/bin/sphinx-build + +.IGNORE: clean +clean: + find src -name '__pycache__' -type d -exec rm -fr {} \; + find tests -name '__pycache__' -type d -exec rm -fr {} \; + rm -rf .venv .coverage *.egg-info .pytest_cache .ruff_cache build dist diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..6287d95a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,73 @@ +[project] +dynamic = ["version", "dependencies", "readme"] +name = "cornice" +description = "Define Web Services in Pyramid." +license = {file = "LICENSE"} +classifiers = [ + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: Implementation :: CPython", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + "Framework :: Pylons", + "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", +] +keywords = ["web pyramid pylons"] +authors = [ + {name = "Mozilla Services", email = "services-dev@mozilla.org"}, +] + +[project.urls] +Repository = "https://github.com/Cornices/cornice" + +[tool.setuptools_scm] +# can be empty if no extra settings are needed, presence enables setuptools_scm + +[tool.setuptools.dynamic] +dependencies = { file = ["requirements.in"] } +readme = {file = ["README.rst", "CONTRIBUTORS.txt"]} + +[build-system] +requires = ["setuptools>=64", "setuptools_scm>=8"] +build-backend = "setuptools.build_meta" + +[project.optional-dependencies] +dev = [ + "ruff", + "pytest", + "pytest-cache", + "pytest-cov", + "webtest", + "marshmallow<4", + "colander", +] + +[tool.pip-tools] +generate-hashes = true + +[tool.coverage.run] +relative_files = true + +[tool.ruff] +line-length = 99 +extend-exclude = [ + "__pycache__", + ".venv/", +] + +[tool.ruff.lint] +select = [ + # pycodestyle + "E", "W", + # flake8 + "F", + # isort + "I", +] +ignore = [ + # `format` will wrap lines. + "E501", +] + +[tool.ruff.lint.isort] +lines-after-imports = 2 diff --git a/requirements.in b/requirements.in new file mode 100644 index 00000000..106c10cc --- /dev/null +++ b/requirements.in @@ -0,0 +1 @@ +pyramid>=1.7,<3 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..26035804 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,87 @@ +# +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: +# +# pip-compile --generate-hashes +# +hupper==1.12.1 \ + --hash=sha256:06bf54170ff4ecf4c84ad5f188dee3901173ab449c2608ad05b9bfd6b13e32eb \ + --hash=sha256:e872b959f09d90be5fb615bd2e62de89a0b57efc037bdf9637fb09cdf8552b19 + # via pyramid +pastedeploy==3.1.0 \ + --hash=sha256:76388ad53a661448d436df28c798063108f70e994ddc749540d733cdbd1b38cf \ + --hash=sha256:9ddbaf152f8095438a9fe81f82c78a6714b92ae8e066bed418b6a7ff6a095a95 + # via plaster-pastedeploy +plaster==1.1.2 \ + --hash=sha256:42992ab1f4865f1278e2ad740e8ad145683bb4022e03534265528f0c23c0df2d \ + --hash=sha256:f8befc54bf8c1147c10ab40297ec84c2676fa2d4ea5d6f524d9436a80074ef98 + # via + # plaster-pastedeploy + # pyramid +plaster-pastedeploy==1.0.1 \ + --hash=sha256:ad3550cc744648969ed3b810f33c9344f515ee8d8a8cec18e8f2c4a643c2181f \ + --hash=sha256:be262e6d2e41a7264875daa2fe2850cbb0615728bcdc92828fdc72736e381412 + # via pyramid +pyramid==2.0.2 \ + --hash=sha256:2e6585ac55c147f0a51bc00dadf72075b3bdd9a871b332ff9e5e04117ccd76fa \ + --hash=sha256:372138a738e4216535cc76dcce6eddd5a1aaca95130f2354fb834264c06f18de + # via -r requirements.in +translationstring==1.4 \ + --hash=sha256:5f4dc4d939573db851c8d840551e1a0fb27b946afe3b95aafc22577eed2d6262 \ + --hash=sha256:bf947538d76e69ba12ab17283b10355a9ecfbc078e6123443f43f2107f6376f3 + # via pyramid +venusian==3.1.0 \ + --hash=sha256:d1fb1e49927f42573f6c9b7c4fcf61c892af8fdcaa2314daa01d9a560b23488d \ + --hash=sha256:eb72cdca6f3139a15dc80f9c95d3c10f8a54a0ba881eeef8e2ec5b42d3ee3a95 + # via pyramid +webob==1.8.7 \ + --hash=sha256:73aae30359291c14fa3b956f8b5ca31960e420c28c1bec002547fb04928cf89b \ + --hash=sha256:b64ef5141be559cfade448f044fa45c2260351edcb6a8ef6b7e00c7dcef0c323 + # via pyramid +zope-deprecation==5.0 \ + --hash=sha256:28c2ee983812efb4676d33c7a8c6ade0df191c1c6d652bbbfe6e2eeee067b2d4 \ + --hash=sha256:b7c32d3392036b2145c40b3103e7322db68662ab09b7267afe1532a9d93f640f + # via pyramid +zope-interface==6.1 \ + --hash=sha256:0c8cf55261e15590065039696607f6c9c1aeda700ceee40c70478552d323b3ff \ + --hash=sha256:13b7d0f2a67eb83c385880489dbb80145e9d344427b4262c49fbf2581677c11c \ + --hash=sha256:1f294a15f7723fc0d3b40701ca9b446133ec713eafc1cc6afa7b3d98666ee1ac \ + --hash=sha256:239a4a08525c080ff833560171d23b249f7f4d17fcbf9316ef4159f44997616f \ + --hash=sha256:2f8d89721834524a813f37fa174bac074ec3d179858e4ad1b7efd4401f8ac45d \ + --hash=sha256:2fdc7ccbd6eb6b7df5353012fbed6c3c5d04ceaca0038f75e601060e95345309 \ + --hash=sha256:34c15ca9248f2e095ef2e93af2d633358c5f048c49fbfddf5fdfc47d5e263736 \ + --hash=sha256:387545206c56b0315fbadb0431d5129c797f92dc59e276b3ce82db07ac1c6179 \ + --hash=sha256:43b576c34ef0c1f5a4981163b551a8781896f2a37f71b8655fd20b5af0386abb \ + --hash=sha256:57d0a8ce40ce440f96a2c77824ee94bf0d0925e6089df7366c2272ccefcb7941 \ + --hash=sha256:5a804abc126b33824a44a7aa94f06cd211a18bbf31898ba04bd0924fbe9d282d \ + --hash=sha256:67be3ca75012c6e9b109860820a8b6c9a84bfb036fbd1076246b98e56951ca92 \ + --hash=sha256:6af47f10cfc54c2ba2d825220f180cc1e2d4914d783d6fc0cd93d43d7bc1c78b \ + --hash=sha256:6dc998f6de015723196a904045e5a2217f3590b62ea31990672e31fbc5370b41 \ + --hash=sha256:70d2cef1bf529bff41559be2de9d44d47b002f65e17f43c73ddefc92f32bf00f \ + --hash=sha256:7ebc4d34e7620c4f0da7bf162c81978fce0ea820e4fa1e8fc40ee763839805f3 \ + --hash=sha256:964a7af27379ff4357dad1256d9f215047e70e93009e532d36dcb8909036033d \ + --hash=sha256:97806e9ca3651588c1baaebb8d0c5ee3db95430b612db354c199b57378312ee8 \ + --hash=sha256:9b9bc671626281f6045ad61d93a60f52fd5e8209b1610972cf0ef1bbe6d808e3 \ + --hash=sha256:9ffdaa5290422ac0f1688cb8adb1b94ca56cee3ad11f29f2ae301df8aecba7d1 \ + --hash=sha256:a0da79117952a9a41253696ed3e8b560a425197d4e41634a23b1507efe3273f1 \ + --hash=sha256:a41f87bb93b8048fe866fa9e3d0c51e27fe55149035dcf5f43da4b56732c0a40 \ + --hash=sha256:aa6fd016e9644406d0a61313e50348c706e911dca29736a3266fc9e28ec4ca6d \ + --hash=sha256:ad54ed57bdfa3254d23ae04a4b1ce405954969c1b0550cc2d1d2990e8b439de1 \ + --hash=sha256:b012d023b4fb59183909b45d7f97fb493ef7a46d2838a5e716e3155081894605 \ + --hash=sha256:b51b64432eed4c0744241e9ce5c70dcfecac866dff720e746d0a9c82f371dfa7 \ + --hash=sha256:bbe81def9cf3e46f16ce01d9bfd8bea595e06505e51b7baf45115c77352675fd \ + --hash=sha256:c9559138690e1bd4ea6cd0954d22d1e9251e8025ce9ede5d0af0ceae4a401e43 \ + --hash=sha256:e30506bcb03de8983f78884807e4fd95d8db6e65b69257eea05d13d519b83ac0 \ + --hash=sha256:e33e86fd65f369f10608b08729c8f1c92ec7e0e485964670b4d2633a4812d36b \ + --hash=sha256:e441e8b7d587af0414d25e8d05e27040d78581388eed4c54c30c0c91aad3a379 \ + --hash=sha256:e8bb9c990ca9027b4214fa543fd4025818dc95f8b7abce79d61dc8a2112b561a \ + --hash=sha256:ef43ee91c193f827e49599e824385ec7c7f3cd152d74cb1dfe02cb135f264d83 \ + --hash=sha256:ef467d86d3cfde8b39ea1b35090208b0447caaabd38405420830f7fd85fbdd56 \ + --hash=sha256:f89b28772fc2562ed9ad871c865f5320ef761a7fcc188a935e21fe8b31a38ca9 \ + --hash=sha256:fddbab55a2473f1d3b8833ec6b7ac31e8211b0aa608df5ab09ce07f3727326de + # via pyramid + +# WARNING: The following packages were not pinned, but pip requires them to be +# pinned when the requirements file includes hashes and the requirement is not +# satisfied by a package already installed. Consider using the --allow-unsafe flag. +# setuptools diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 81049c3a..00000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[zest.releaser] -create-wheel = yes diff --git a/setup.py b/setup.py deleted file mode 100644 index c84e673d..00000000 --- a/setup.py +++ /dev/null @@ -1,44 +0,0 @@ -import os -from setuptools import setup, find_packages - -here = os.path.abspath(os.path.dirname(__file__)) - -with open(os.path.join(here, 'README.rst')) as f: - README = f.read() - -with open(os.path.join(here, 'CHANGES.txt')) as f: - CHANGES = f.read() - -requires = ['pyramid>=1.7,<3', 'venusian'] - -entry_points = "" -package_data = {} - -setup(name='cornice', - version='6.0.2.dev0', - description='Define Web Services in Pyramid.', - long_description=README + '\n\n' + CHANGES, - license='MPLv2.0', - classifiers=[ - "Programming Language :: Python", - "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", - "Framework :: Pylons", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", - "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", ], - python_requires='>=3.5', - entry_points=entry_points, - author='Mozilla Services', - author_email='services-dev@mozilla.org', - url='https://github.com/mozilla-services/cornice', - keywords='web pyramid pylons', - packages=find_packages(exclude=("tests",)), - package_data=package_data, - include_package_data=True, - zip_safe=False, - install_requires=requires) diff --git a/cornice/__init__.py b/src/cornice/__init__.py similarity index 100% rename from cornice/__init__.py rename to src/cornice/__init__.py diff --git a/cornice/cors.py b/src/cornice/cors.py similarity index 100% rename from cornice/cors.py rename to src/cornice/cors.py diff --git a/cornice/errors.py b/src/cornice/errors.py similarity index 100% rename from cornice/errors.py rename to src/cornice/errors.py diff --git a/cornice/pyramidhook.py b/src/cornice/pyramidhook.py similarity index 100% rename from cornice/pyramidhook.py rename to src/cornice/pyramidhook.py diff --git a/cornice/renderer.py b/src/cornice/renderer.py similarity index 100% rename from cornice/renderer.py rename to src/cornice/renderer.py diff --git a/cornice/resource.py b/src/cornice/resource.py similarity index 100% rename from cornice/resource.py rename to src/cornice/resource.py diff --git a/cornice/service.py b/src/cornice/service.py similarity index 100% rename from cornice/service.py rename to src/cornice/service.py diff --git a/cornice/util.py b/src/cornice/util.py similarity index 100% rename from cornice/util.py rename to src/cornice/util.py diff --git a/cornice/validators/__init__.py b/src/cornice/validators/__init__.py similarity index 100% rename from cornice/validators/__init__.py rename to src/cornice/validators/__init__.py diff --git a/cornice/validators/_colander.py b/src/cornice/validators/_colander.py similarity index 100% rename from cornice/validators/_colander.py rename to src/cornice/validators/_colander.py diff --git a/cornice/validators/_marshmallow.py b/src/cornice/validators/_marshmallow.py similarity index 100% rename from cornice/validators/_marshmallow.py rename to src/cornice/validators/_marshmallow.py diff --git a/tests/requirements.txt b/tests/requirements.txt deleted file mode 100644 index a16548c3..00000000 --- a/tests/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -Sphinx -WebTest -colander>=1.0b1 -marshmallow>=3.0.0rc1,<4.0 -coverage -pytest -pytest-cov -simplejson diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 8becaa88..00000000 --- a/tox.ini +++ /dev/null @@ -1,30 +0,0 @@ -[tox] -envlist = py38-raw,py38,py39,py310,py311,py312,flake8,docs -skip_missing_interpreters = True - -[testenv] -commands = - python --version - pytest {posargs: --cov-report term-missing --cov-fail-under 100 --cov cornice} -deps = - -rtests/requirements.txt -install_command = pip install --pre {opts} {packages} - -[testenv:py37-raw] -deps = - pytest - pytest-cov - webtest - mock -install_command = pip install --pre {opts} {packages} -commands = - python --version - pytest {posargs} - -[testenv:flake8] -commands = flake8 cornice -deps = - flake8 - -[testenv:docs] -commands = /usr/bin/make docs From 81c248d216819551f3e189377ee89ca27a10a370 Mon Sep 17 00:00:00 2001 From: Mathieu Leplatre Date: Tue, 30 Jan 2024 19:47:15 +0100 Subject: [PATCH 2/9] Run 'make format' --- src/cornice/__init__.py | 67 ++- src/cornice/cors.py | 87 ++- src/cornice/errors.py | 18 +- src/cornice/pyramidhook.py | 159 ++--- src/cornice/renderer.py | 13 +- src/cornice/resource.py | 65 ++- src/cornice/service.py | 130 +++-- src/cornice/util.py | 39 +- src/cornice/validators/__init__.py | 79 ++- src/cornice/validators/_colander.py | 23 +- src/cornice/validators/_marshmallow.py | 30 +- tests/support.py | 20 +- tests/test_cors.py | 384 ++++++------- tests/test_errors.py | 35 +- tests/test_imperative_resource.py | 127 ++-- tests/test_init.py | 32 +- tests/test_pyramidhook.py | 209 +++---- tests/test_renderer.py | 14 +- tests/test_resource.py | 166 +++--- tests/test_resource_callable.py | 87 +-- tests/test_resource_custom_predicates.py | 145 +++-- tests/test_resource_traverse.py | 42 +- tests/test_service.py | 395 +++++++------ tests/test_service_definition.py | 31 +- tests/test_util.py | 12 +- tests/test_validation.py | 703 +++++++++++------------ tests/validationapp.py | 230 ++++---- 27 files changed, 1638 insertions(+), 1704 deletions(-) diff --git a/src/cornice/__init__.py b/src/cornice/__init__.py index 4c09ef99..f77f1715 100644 --- a/src/cornice/__init__.py +++ b/src/cornice/__init__.py @@ -2,32 +2,32 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. import logging -import pkg_resources from functools import partial +import pkg_resources +from pyramid.events import NewRequest +from pyramid.httpexceptions import HTTPForbidden, HTTPNotFound +from pyramid.security import NO_PERMISSION_REQUIRED +from pyramid.settings import asbool, aslist + from cornice.errors import Errors # NOQA -from cornice.renderer import CorniceRenderer -from cornice.service import Service # NOQA from cornice.pyramidhook import ( - wrap_request, - register_service_views, handle_exceptions, register_resource_views, + register_service_views, + wrap_request, ) +from cornice.renderer import CorniceRenderer +from cornice.service import Service # NOQA from cornice.util import ContentTypePredicate, current_service -from pyramid.events import NewRequest -from pyramid.httpexceptions import HTTPNotFound, HTTPForbidden -from pyramid.security import NO_PERMISSION_REQUIRED -from pyramid.settings import aslist, asbool -logger = logging.getLogger('cornice') +logger = logging.getLogger("cornice") # Module version, as defined in PEP-0396. __version__ = pkg_resources.get_distribution(__package__).version -def set_localizer_for_languages(event, available_languages, - default_locale_name): +def set_localizer_for_languages(event, available_languages, default_locale_name): """ Sets the current locale based on the incoming Accept-Language header, if present, and sets a localizer attribute on the request object based on @@ -40,8 +40,7 @@ def set_localizer_for_languages(event, available_languages, """ request = event.request if request.accept_language: - accepted = request.accept_language.lookup(available_languages, - default=default_locale_name) + accepted = request.accept_language.lookup(available_languages, default=default_locale_name) request._LOCALE_ = accepted @@ -54,13 +53,15 @@ def setup_localization(config): and Localization" section of the Pyramid documentation. """ try: - config.add_translation_dirs('colander:locale/') + config.add_translation_dirs("colander:locale/") settings = config.get_settings() - available_languages = aslist(settings['available_languages']) - default_locale_name = settings.get('pyramid.default_locale_name', 'en') - set_localizer = partial(set_localizer_for_languages, - available_languages=available_languages, - default_locale_name=default_locale_name) + available_languages = aslist(settings["available_languages"]) + default_locale_name = settings.get("pyramid.default_locale_name", "en") + set_localizer = partial( + set_localizer_for_languages, + available_languages=available_languages, + default_locale_name=default_locale_name, + ) config.add_subscriber(set_localizer, NewRequest) except ImportError: # pragma: no cover # add_translation_dirs raises an ImportError if colander is not @@ -69,8 +70,7 @@ def setup_localization(config): def includeme(config): - """Include the Cornice definitions - """ + """Include the Cornice definitions""" # attributes required to maintain services config.registry.cornice_services = {} @@ -78,20 +78,19 @@ def includeme(config): # localization request subscriber must be set before first call # for request.localizer (in wrap_request) - if settings.get('available_languages'): + if settings.get("available_languages"): setup_localization(config) - config.add_directive('add_cornice_service', register_service_views) - config.add_directive('add_cornice_resource', register_resource_views) + config.add_directive("add_cornice_service", register_service_views) + config.add_directive("add_cornice_resource", register_resource_views) config.add_subscriber(wrap_request, NewRequest) - config.add_renderer('cornicejson', CorniceRenderer()) - config.add_view_predicate('content_type', ContentTypePredicate) + config.add_renderer("cornicejson", CorniceRenderer()) + config.add_view_predicate("content_type", ContentTypePredicate) config.add_request_method(current_service, reify=True) - if asbool(settings.get('handle_exceptions', True)): - config.add_view(handle_exceptions, context=Exception, - permission=NO_PERMISSION_REQUIRED) - config.add_view(handle_exceptions, context=HTTPNotFound, - permission=NO_PERMISSION_REQUIRED) - config.add_view(handle_exceptions, context=HTTPForbidden, - permission=NO_PERMISSION_REQUIRED) + if asbool(settings.get("handle_exceptions", True)): + config.add_view(handle_exceptions, context=Exception, permission=NO_PERMISSION_REQUIRED) + config.add_view(handle_exceptions, context=HTTPNotFound, permission=NO_PERMISSION_REQUIRED) + config.add_view( + handle_exceptions, context=HTTPForbidden, permission=NO_PERMISSION_REQUIRED + ) diff --git a/src/cornice/cors.py b/src/cornice/cors.py index dd75c19e..1f5073e8 100644 --- a/src/cornice/cors.py +++ b/src/cornice/cors.py @@ -7,9 +7,14 @@ from pyramid.settings import asbool -CORS_PARAMETERS = ('cors_headers', 'cors_enabled', 'cors_origins', - 'cors_credentials', 'cors_max_age', - 'cors_expose_all_headers') +CORS_PARAMETERS = ( + "cors_headers", + "cors_enabled", + "cors_origins", + "cors_credentials", + "cors_max_age", + "cors_expose_all_headers", +) def get_cors_preflight_view(service): @@ -22,52 +27,48 @@ def get_cors_preflight_view(service): def _preflight_view(request): response = request.response - origin = request.headers.get('Origin') + origin = request.headers.get("Origin") supported_headers = service.cors_supported_headers_for() if not origin: - request.errors.add('header', 'Origin', - 'this header is mandatory') + request.errors.add("header", "Origin", "this header is mandatory") - requested_method = request.headers.get('Access-Control-Request-Method') + requested_method = request.headers.get("Access-Control-Request-Method") if not requested_method: - request.errors.add('header', 'Access-Control-Request-Method', - 'this header is mandatory') + request.errors.add( + "header", "Access-Control-Request-Method", "this header is mandatory" + ) if not (requested_method and origin): return - requested_headers = ( - request.headers.get('Access-Control-Request-Headers', ())) + requested_headers = request.headers.get("Access-Control-Request-Headers", ()) if requested_headers: - requested_headers = map(str.strip, requested_headers.split(',')) + requested_headers = map(str.strip, requested_headers.split(",")) if requested_method not in service.cors_supported_methods: - request.errors.add('header', 'Access-Control-Request-Method', - 'Method not allowed') + request.errors.add("header", "Access-Control-Request-Method", "Method not allowed") if not service.cors_expose_all_headers: for h in requested_headers: - if not h.lower() in [s.lower() for s in supported_headers]: + if h.lower() not in [s.lower() for s in supported_headers]: request.errors.add( - 'header', - 'Access-Control-Request-Headers', - 'Header "%s" not allowed' % h) + "header", "Access-Control-Request-Headers", 'Header "%s" not allowed' % h + ) supported_headers = set(supported_headers) | set(requested_headers) - response.headers['Access-Control-Allow-Headers'] = ( - ','.join(supported_headers)) + response.headers["Access-Control-Allow-Headers"] = ",".join(supported_headers) - response.headers['Access-Control-Allow-Methods'] = ( - ','.join(service.cors_supported_methods)) + response.headers["Access-Control-Allow-Methods"] = ",".join(service.cors_supported_methods) max_age = service.cors_max_age_for(requested_method) if max_age is not None: - response.headers['Access-Control-Max-Age'] = str(max_age) + response.headers["Access-Control-Max-Age"] = str(max_age) return None + return _preflight_view @@ -76,9 +77,8 @@ def _get_method(request): (e.g if the verb is options, look at the A-C-Request-Method header, otherwise return the HTTP verb). """ - if request.method == 'OPTIONS': - method = request.headers.get('Access-Control-Request-Method', - request.method) + if request.method == "OPTIONS": + method = request.headers.get("Access-Control-Request-Method", request.method) else: method = request.method return method @@ -89,15 +89,13 @@ def ensure_origin(service, request, response=None, **kwargs): response = response or request.response # Don't check this twice. - if not request.info.get('cors_checked', False): + if not request.info.get("cors_checked", False): method = _get_method(request) - origin = request.headers.get('Origin') + origin = request.headers.get("Origin") if not origin: - always_cors = asbool( - request.registry.settings.get("cornice.always_cors") - ) + always_cors = asbool(request.registry.settings.get("cornice.always_cors")) # With this setting, if the service origins has "*", then # always return CORS headers. origins = getattr(service, "cors_origins", []) @@ -105,18 +103,16 @@ def ensure_origin(service, request, response=None, **kwargs): origin = "*" if origin: - if not any([fnmatch.fnmatchcase(origin, o) - for o in service.cors_origins_for(method)]): - request.errors.add('header', 'Origin', - '%s not allowed' % origin) + if not any([fnmatch.fnmatchcase(origin, o) for o in service.cors_origins_for(method)]): + request.errors.add("header", "Origin", "%s not allowed" % origin) elif service.cors_support_credentials_for(method): - response.headers['Access-Control-Allow-Origin'] = origin + response.headers["Access-Control-Allow-Origin"] = origin else: if any([o == "*" for o in service.cors_origins_for(method)]): - response.headers['Access-Control-Allow-Origin'] = '*' + response.headers["Access-Control-Allow-Origin"] = "*" else: - response.headers['Access-Control-Allow-Origin'] = origin - request.info['cors_checked'] = True + response.headers["Access-Control-Allow-Origin"] = origin + request.info["cors_checked"] = True return response @@ -133,15 +129,16 @@ def apply_cors_post_request(service, request, response): response = ensure_origin(service, request, response) method = _get_method(request) - if (service.cors_support_credentials_for(method) and - 'Access-Control-Allow-Credentials' not in response.headers): - response.headers['Access-Control-Allow-Credentials'] = 'true' + if ( + service.cors_support_credentials_for(method) + and "Access-Control-Allow-Credentials" not in response.headers + ): + response.headers["Access-Control-Allow-Credentials"] = "true" - if request.method != 'OPTIONS': + if request.method != "OPTIONS": # Which headers are exposed? supported_headers = service.cors_supported_headers_for(request.method) if supported_headers: - response.headers['Access-Control-Expose-Headers'] = ( - ', '.join(supported_headers)) + response.headers["Access-Control-Expose-Headers"] = ", ".join(supported_headers) return response diff --git a/src/cornice/errors.py b/src/cornice/errors.py index 8a735a39..746ea723 100644 --- a/src/cornice/errors.py +++ b/src/cornice/errors.py @@ -7,8 +7,8 @@ class Errors(list): - """Holds Request errors - """ + """Holds Request errors""" + def __init__(self, status=400, localizer=None): self.status = status self.localizer = localizer @@ -16,24 +16,20 @@ def __init__(self, status=400, localizer=None): def add(self, location, name=None, description=None, **kw): """Registers a new error.""" - allowed = ('body', 'querystring', 'url', 'header', 'path', - 'cookies', 'method') - if location != '' and location not in allowed: - raise ValueError('%r not in %s' % (location, allowed)) + allowed = ("body", "querystring", "url", "header", "path", "cookies", "method") + if location != "" and location not in allowed: + raise ValueError("%r not in %s" % (location, allowed)) if isinstance(description, TranslationString) and self.localizer: description = self.localizer.translate(description) - self.append(dict( - location=location, - name=name, - description=description, **kw)) + self.append(dict(location=location, name=name, description=description, **kw)) @classmethod def from_json(cls, string): """Transforms a json string into an `Errors` instance""" obj = json.loads(string.decode()) - return Errors.from_list(obj.get('errors', [])) + return Errors.from_list(obj.get("errors", [])) @classmethod def from_list(cls, obj): diff --git a/src/cornice/pyramidhook.py b/src/cornice/pyramidhook.py index 13a996de..7ef351c4 100644 --- a/src/cornice/pyramidhook.py +++ b/src/cornice/pyramidhook.py @@ -1,26 +1,34 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. -import functools import copy +import functools import itertools -from pyramid.httpexceptions import (HTTPMethodNotAllowed, HTTPNotAcceptable, - HTTPUnsupportedMediaType, HTTPException) from pyramid.exceptions import PredicateMismatch +from pyramid.httpexceptions import ( + HTTPException, + HTTPMethodNotAllowed, + HTTPNotAcceptable, + HTTPUnsupportedMediaType, +) from pyramid.security import NO_PERMISSION_REQUIRED -from cornice.service import decorate_view -from cornice.errors import Errors -from cornice.util import ( - is_string, to_list, match_accept_header, match_content_type_header, - content_type_matches, current_service -) from cornice.cors import ( - get_cors_validator, - get_cors_preflight_view, + CORS_PARAMETERS, apply_cors_post_request, - CORS_PARAMETERS + get_cors_preflight_view, + get_cors_validator, +) +from cornice.errors import Errors +from cornice.service import decorate_view +from cornice.util import ( + content_type_matches, + current_service, + is_string, + match_accept_header, + match_content_type_header, + to_list, ) @@ -48,37 +56,38 @@ def _fallback_view(request): if method != request.method: continue - if 'accept' in args: - acceptable.extend( - service.get_acceptable(method, filter_callables=True)) - acceptable.extend( - request.info.get('acceptable', [])) + if "accept" in args: + acceptable.extend(service.get_acceptable(method, filter_callables=True)) + acceptable.extend(request.info.get("acceptable", [])) acceptable = list(set(acceptable)) # Now check if that was actually the source of the problem. if not request.accept.acceptable_offers(offers=acceptable): request.errors.add( - 'header', 'Accept', - 'Accept header should be one of {0}'.format( - acceptable).encode('ascii')) + "header", + "Accept", + "Accept header should be one of {0}".format(acceptable).encode("ascii"), + ) request.errors.status = HTTPNotAcceptable.code error = service.error_handler(request) raise error - if 'content_type' in args: + if "content_type" in args: supported_contenttypes.extend( - service.get_contenttypes(method, - filter_callables=True)) - supported_contenttypes.extend( - request.info.get('supported_contenttypes', [])) + service.get_contenttypes(method, filter_callables=True) + ) + supported_contenttypes.extend(request.info.get("supported_contenttypes", [])) supported_contenttypes = list(set(supported_contenttypes)) # Now check if that was actually the source of the problem. if not content_type_matches(request, supported_contenttypes): request.errors.add( - 'header', 'Content-Type', - 'Content-Type header should be one of {0}'.format( - supported_contenttypes).encode('ascii')) + "header", + "Content-Type", + "Content-Type header should be one of {0}".format( + supported_contenttypes + ).encode("ascii"), + ) request.errors.status = HTTPUnsupportedMediaType.code error = service.error_handler(request) raise error @@ -92,6 +101,7 @@ def _fallback_view(request): # order to avoid unpredictable cases, we left this line in place and # excluded it from coverage. raise PredicateMismatch(service.name) # pragma: no cover + return _fallback_view @@ -101,7 +111,7 @@ def apply_filters(request, response): service = current_service(request) if service is not None: kwargs, ob = getattr(request, "cornice_args", ({}, None)) - for _filter in kwargs.get('filters', []): + for _filter in kwargs.get("filters", []): if is_string(_filter) and ob is not None: _filter = getattr(ob, _filter) try: @@ -119,7 +129,7 @@ def handle_exceptions(exc, request): # a new response started (the exception), so we need to do that again. if not isinstance(exc, HTTPException): raise - request.info['cors_checked'] = False + request.info["cors_checked"] = False return apply_filters(request, exc) @@ -143,17 +153,17 @@ def wrap_request(event): request.add_response_callback(apply_filters) request.add_response_callback(add_nosniff_header) - if not hasattr(request, 'validated'): - setattr(request, 'validated', {}) + if not hasattr(request, "validated"): + setattr(request, "validated", {}) - if not hasattr(request, 'errors'): + if not hasattr(request, "errors"): if request.registry.settings.get("available_languages"): - setattr(request, 'errors', Errors(localizer=request.localizer)) + setattr(request, "errors", Errors(localizer=request.localizer)) else: - setattr(request, 'errors', Errors()) + setattr(request, "errors", Errors()) - if not hasattr(request, 'info'): - setattr(request, 'info', {}) + if not hasattr(request, "info"): + setattr(request, "info", {}) def register_service_views(config, service): @@ -164,41 +174,48 @@ def register_service_views(config, service): """ route_name = service.name existing_route = service.pyramid_route - prefix = config.route_prefix or '' + prefix = config.route_prefix or "" services = config.registry.cornice_services if existing_route: route_name = existing_route - services['__cornice' + existing_route] = service + services["__cornice" + existing_route] = service else: services[prefix + service.path] = service # before doing anything else, register a view for the OPTIONS method # if we need to - if service.cors_enabled and 'OPTIONS' not in service.defined_methods: - service.add_view('options', view=get_cors_preflight_view(service), - permission=NO_PERMISSION_REQUIRED) + if service.cors_enabled and "OPTIONS" not in service.defined_methods: + service.add_view( + "options", view=get_cors_preflight_view(service), permission=NO_PERMISSION_REQUIRED + ) # register the fallback view, which takes care of returning good error # messages to the user-agent cors_validator = get_cors_validator(service) # Cornice-specific arguments that pyramid does not know about - cornice_parameters = ('filters', 'validators', 'schema', 'klass', - 'error_handler', 'deserializer') + CORS_PARAMETERS + cornice_parameters = ( + "filters", + "validators", + "schema", + "klass", + "error_handler", + "deserializer", + ) + CORS_PARAMETERS # 1. register route route_args = {} - if hasattr(service, 'factory'): - route_args['factory'] = service.factory + if hasattr(service, "factory"): + route_args["factory"] = service.factory - routes = config.get_predlist('route') + routes = config.get_predlist("route") for predicate in routes.sorter.names: # Do not let the custom predicates handle validation of Header Accept, # which will pass it through to pyramid. It is handled by # _fallback_view(), because it allows callable. - if predicate == 'accept': + if predicate == "accept": continue if hasattr(service, predicate): @@ -211,7 +228,6 @@ def register_service_views(config, service): # 2. register view(s) for method, view, args in service.definitions: - args = copy.copy(args) # make a copy of the dict to not modify it # Deepcopy only the params we're possibly passing on to pyramid # (Some of those in cornice_parameters, e.g. ``schema``, may contain @@ -220,10 +236,10 @@ def register_service_views(config, service): if item not in cornice_parameters: args[item] = copy.deepcopy(args[item]) - args['request_method'] = method + args["request_method"] = method if service.cors_enabled: - args['validators'].insert(0, cors_validator) + args["validators"].insert(0, cors_validator) decorated_view = decorate_view(view, dict(args), method, route_args) @@ -232,8 +248,8 @@ def register_service_views(config, service): del args[item] # filter predicates defined on Resource - route_predicates = config.get_predlist('route').sorter.names - view_predicates = config.get_predlist('view').sorter.names + route_predicates = config.get_predlist("route").sorter.names + view_predicates = config.get_predlist("view").sorter.names for pred in set(route_predicates).difference(view_predicates): if pred in args: args.pop(pred) @@ -243,7 +259,7 @@ def register_service_views(config, service): predicate_definitions = _pop_complex_predicates(args) if predicate_definitions: - empty_contenttype = [({'kind': 'content_type', 'value': ''},)] + empty_contenttype = [({"kind": "content_type", "value": ""},)] for predicate_list in predicate_definitions + empty_contenttype: args = dict(args) # make a copy of the dict to not modify it @@ -252,21 +268,21 @@ def register_service_views(config, service): # We register the same view multiple times with different # accept / content_type / custom_predicates arguments - config.add_view(view=decorated_view, route_name=route_name, - **args) + config.add_view(view=decorated_view, route_name=route_name, **args) else: # it is a simple view, we don't need to loop on the definitions # and just add it one time. - config.add_view(view=decorated_view, route_name=route_name, - **args) + config.add_view(view=decorated_view, route_name=route_name, **args) if service.definitions: # Add the fallback view last - config.add_view(view=get_fallback_view(service), - route_name=route_name, - permission=NO_PERMISSION_REQUIRED, - require_csrf=False) + config.add_view( + view=get_fallback_view(service), + route_name=route_name, + permission=NO_PERMISSION_REQUIRED, + require_csrf=False, + ) def _pop_complex_predicates(args): @@ -280,8 +296,8 @@ def _pop_complex_predicates(args): """ # pop and prepare individual predicate lists - accept_list = _pop_predicate_definition(args, 'accept') - content_type_list = _pop_predicate_definition(args, 'content_type') + accept_list = _pop_predicate_definition(args, "accept") + content_type_list = _pop_predicate_definition(args, "content_type") # compute cartesian product of prepared lists, additionally # remove empty elements of input and output lists @@ -305,7 +321,7 @@ def _pop_predicate_definition(args, kind): # returns an iterator. (In Python 2, it returned a list.) # http://getpython3.com/diveintopython3/ \ # porting-code-to-python-3-with-2to3.html#map - values = list(map(lambda value: {'kind': kind, 'value': value}, values)) + values = list(map(lambda value: {"kind": kind, "value": value}, values)) return values @@ -322,23 +338,22 @@ def _mungle_view_args(args, predicate_list): # map kind of argument value to function for resolving callables callable_map = { - 'accept': match_accept_header, - 'content_type': match_content_type_header, + "accept": match_accept_header, + "content_type": match_content_type_header, } # iterate and resolve all predicates for predicate_entry in predicate_list: - - kind = predicate_entry['kind'] - value = predicate_entry['value'] + kind = predicate_entry["kind"] + value = predicate_entry["value"] # we need to build a custom predicate if argument value is a callable - predicates = args.get('custom_predicates', []) + predicates = args.get("custom_predicates", []) if callable(value): func = callable_map[kind] predicate_checker = functools.partial(func, value) predicates.append(predicate_checker) - args['custom_predicates'] = predicates + args["custom_predicates"] = predicates else: # otherwise argument value is just a scalar args[kind] = value diff --git a/src/cornice/renderer.py b/src/cornice/renderer.py index f06fcb12..1fd1ffb4 100644 --- a/src/cornice/renderer.py +++ b/src/cornice/renderer.py @@ -6,16 +6,16 @@ def bytes_adapter(obj, request): """Convert bytes objects to strings for json error renderer.""" if isinstance(obj, bytes): - return obj.decode('utf8') + return obj.decode("utf8") return obj class JSONError(exc.HTTPError): def __init__(self, serializer, serializer_kw, errors, status=400): - body = {'status': 'error', 'errors': errors} + body = {"status": "error", "errors": errors} Response.__init__(self, serializer(body, **serializer_kw)) self.status = status - self.content_type = 'application/json' + self.content_type = "application/json" class CorniceRenderer(JSON): @@ -30,7 +30,8 @@ class CorniceRenderer(JSON): .. _`[2]`: http://pyramid.readthedocs.io/en/latest/narr/renderers.html \ #serializing-custom-objects """ - acceptable = ('application/json', 'text/plain') + + acceptable = ("application/json", "text/plain") def __init__(self, *args, **kwargs): """Adds a `bytes` adapter by default.""" @@ -49,7 +50,7 @@ def render_errors(self, request): serializer=self.serializer, serializer_kw=serializer_kw, errors=request.errors, - status=request.errors.status + status=request.errors.status, ) def render(self, value, system): @@ -63,7 +64,7 @@ def render(self, value, system): the user specify the Content-Type manually. TODO: maybe explain this a little better """ - request = system.get('request') + request = system.get("request") if request is not None: response = request.response diff --git a/src/cornice/resource.py b/src/cornice/resource.py index e41176ec..e5179017 100644 --- a/src/cornice/resource.py +++ b/src/cornice/resource.py @@ -2,8 +2,8 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. -import warnings import functools +import warnings import venusian @@ -28,8 +28,10 @@ def resource(depth=2, **kw): @resource(collection_path='/users', path='/users/{id}') """ + def wrapper(klass): return add_resource(klass, depth, **kw) + return wrapper @@ -74,56 +76,52 @@ class User(object): services = {} - if (('collection_pyramid_route' in kw or 'pyramid_route' in kw) and - ('collection_path' in kw or 'path' in kw)): - raise ValueError('You use either paths or route names, not both') + if ("collection_pyramid_route" in kw or "pyramid_route" in kw) and ( + "collection_path" in kw or "path" in kw + ): + raise ValueError("You use either paths or route names, not both") - if 'collection_path' in kw: - if kw['collection_path'] == kw['path']: + if "collection_path" in kw: + if kw["collection_path"] == kw["path"]: msg = "Warning: collection_path and path are not distinct." warnings.warn(msg) - prefixes = ('', 'collection_') + prefixes = ("", "collection_") else: - prefixes = ('',) + prefixes = ("",) - if 'collection_pyramid_route' in kw: - if kw['collection_pyramid_route'] == kw['pyramid_route']: - msg = "Warning: collection_pyramid_route and " \ - "pyramid_route are not distinct." + if "collection_pyramid_route" in kw: + if kw["collection_pyramid_route"] == kw["pyramid_route"]: + msg = "Warning: collection_pyramid_route and " "pyramid_route are not distinct." warnings.warn(msg) - prefixes = ('', 'collection_') + prefixes = ("", "collection_") for prefix in prefixes: - # get clean view arguments service_args = {} for k in list(kw): - if k.startswith('collection_'): - if prefix == 'collection_': - service_args[k[len(prefix):]] = kw[k] + if k.startswith("collection_"): + if prefix == "collection_": + service_args[k[len(prefix) :]] = kw[k] elif k not in service_args: service_args[k] = kw[k] # auto-wire klass as its own view factory, unless one # is explicitly declared. - if 'factory' not in kw: - service_args['factory'] = klass + if "factory" not in kw: + service_args["factory"] = klass # create service - service_name = (service_args.pop('name', None) or - klass.__name__.lower()) + service_name = service_args.pop("name", None) or klass.__name__.lower() service_name = prefix + service_name - service = services[service_name] = Service(name=service_name, - depth=depth, **service_args) + service = services[service_name] = Service(name=service_name, depth=depth, **service_args) # ensure the service comes with the same properties as the wrapped # resource functools.update_wrapper(service, klass) # initialize views - for verb in ('get', 'post', 'put', 'delete', 'options', 'patch'): - + for verb in ("get", "post", "put", "delete", "options", "patch"): view_attr = prefix + verb meth = getattr(klass, view_attr, None) @@ -131,15 +129,14 @@ class User(object): # if the method has a __views__ arguments, then it had # been decorated by a @view decorator. get back the name of # the decorated method so we can register it properly - views = getattr(meth, '__views__', []) + views = getattr(meth, "__views__", []) if views: for view_args in views: - service.add_view(verb, view_attr, klass=klass, - **view_args) + service.add_view(verb, view_attr, klass=klass, **view_args) else: service.add_view(verb, view_attr, klass=klass) - setattr(klass, '_services', services) + setattr(klass, "_services", services) def callback(context, name, ob): # get the callbacks registered by the inner services @@ -149,7 +146,7 @@ def callback(context, name, ob): config = context.config.with_package(info.module) config.add_cornice_service(service) - info = venusian.attach(klass, callback, category='pyramid', depth=depth) + info = venusian.attach(klass, callback, category="pyramid", depth=depth) return klass @@ -161,8 +158,10 @@ def view(**kw): :param kw: Keyword arguments configuring the view. """ + def wrapper(func): return add_view(func, **kw) + return wrapper @@ -195,12 +194,12 @@ def get(self): add_resource(User, collection_path='/users', path='/users/{id}') """ # XXX needed in py2 to set on instancemethod - if hasattr(func, '__func__'): # pragma: no cover + if hasattr(func, "__func__"): # pragma: no cover func = func.__func__ # store view argument to use them later in @resource - views = getattr(func, '__views__', None) + views = getattr(func, "__views__", None) if views is None: views = [] - setattr(func, '__views__', views) + setattr(func, "__views__", views) views.append(kw) return func diff --git a/src/cornice/service.py b/src/cornice/service.py index 34e72fe7..8a908361 100644 --- a/src/cornice/service.py +++ b/src/cornice/service.py @@ -2,16 +2,18 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. import functools + +import venusian from pyramid.exceptions import ConfigurationError from pyramid.interfaces import IRendererFactory from pyramid.response import Response + +from cornice.util import func_name, is_string, to_list from cornice.validators import ( - DEFAULT_VALIDATORS, DEFAULT_FILTERS, + DEFAULT_VALIDATORS, ) -import venusian -from cornice.util import is_string, to_list, func_name SERVICES = [] @@ -21,7 +23,6 @@ def clear_services(): def get_services(names=None, exclude=None): - def _keep(service): if exclude is not None and service.name in exclude: # excluded ! @@ -149,25 +150,33 @@ class Service(object): :meth:`~put`, :meth:`~options` and :meth:`~delete` are decorators that can be used to decorate views. """ - renderer = 'cornicejson' + + renderer = "cornicejson" default_validators = DEFAULT_VALIDATORS default_filters = DEFAULT_FILTERS - mandatory_arguments = ('renderer',) - list_arguments = ('validators', 'filters', 'cors_headers', 'cors_origins') + mandatory_arguments = ("renderer",) + list_arguments = ("validators", "filters", "cors_headers", "cors_origins") def __repr__(self): - return u'' % ( - self.name, self.pyramid_route or self.path) - - def __init__(self, name, path=None, description=None, cors_policy=None, - depth=1, pyramid_route=None, **kw): + return "" % (self.name, self.pyramid_route or self.path) + + def __init__( + self, + name, + path=None, + description=None, + cors_policy=None, + depth=1, + pyramid_route=None, + **kw, + ): self.name = name self.path = path self.pyramid_route = pyramid_route if not self.path and not self.pyramid_route: - raise TypeError('You need to pass path or pyramid_route arg') + raise TypeError("You need to pass path or pyramid_route arg") self.description = description self.cors_expose_all_headers = True @@ -175,14 +184,14 @@ def __init__(self, name, path=None, description=None, cors_policy=None, if cors_policy: for key, value in cors_policy.items(): - kw.setdefault('cors_' + key, value) + kw.setdefault("cors_" + key, value) for key in self.list_arguments: # default_{validators,filters} and {filters,validators} don't # have to be mutables, so we need to create a new list from them extra = to_list(kw.get(key, [])) kw[key] = [] - kw[key].extend(getattr(self, 'default_%s' % key, [])) + kw[key].extend(getattr(self, "default_%s" % key, [])) kw[key].extend(extra) self.arguments = self.get_arguments(kw) @@ -190,10 +199,10 @@ def __init__(self, name, path=None, description=None, cors_policy=None, # avoid squashing Service.decorator if ``decorator`` # argument is used to specify a default pyramid view # decorator - if key != 'decorator': + if key != "decorator": setattr(self, key, value) - if hasattr(self, 'acl'): + if hasattr(self, "acl"): raise ConfigurationError("'acl' is not supported") # instantiate some variables we use to keep track of what's defined for @@ -210,7 +219,7 @@ def callback(context, name, ob): config = context.config.with_package(info.module) config.add_cornice_service(self) - info = venusian.attach(self, callback, category='pyramid', depth=depth) + info = venusian.attach(self, callback, category="pyramid", depth=depth) def default_error_handler(self, request): """Default error_handler. @@ -222,9 +231,7 @@ def default_error_handler(self, request): :param request: the current Request. """ - renderer = request.registry.queryUtility( - IRendererFactory, name=self.renderer - ) + renderer = request.registry.queryUtility(IRendererFactory, name=self.renderer) return renderer.render_errors(request) def get_arguments(self, conf=None): @@ -253,15 +260,14 @@ def get_arguments(self, conf=None): arguments[arg] = value # Allow custom error handler - arguments['error_handler'] = conf.pop( - 'error_handler', - getattr(self, 'error_handler', self.default_error_handler) + arguments["error_handler"] = conf.pop( + "error_handler", getattr(self, "error_handler", self.default_error_handler) ) # exclude some validators or filters - if 'exclude' in conf: - for item in to_list(conf.pop('exclude')): - for container in arguments['validators'], arguments['filters']: + if "exclude" in conf: + for item in to_list(conf.pop("exclude")): + for container in arguments["validators"], arguments["filters"]: if item in container: container.remove(item) @@ -270,7 +276,7 @@ def get_arguments(self, conf=None): # if some keys have been defined service-wide, then we need to add # them to the returned dict. - if hasattr(self, 'arguments'): + if hasattr(self, "arguments"): for key, value in self.arguments.items(): if key not in arguments: arguments[key] = value @@ -292,17 +298,17 @@ def add_view(self, method, view, **kwargs): """ method = method.upper() - if 'klass' in kwargs and not callable(view): - view = _UnboundView(kwargs['klass'], view) + if "klass" in kwargs and not callable(view): + view = _UnboundView(kwargs["klass"], view) args = self.get_arguments(kwargs) # remove 'factory' if present, # it's not a valid pyramid view param - if 'factory' in args: - del args['factory'] + if "factory" in args: + del args["factory"] - if hasattr(self, 'get_view_wrapper'): + if hasattr(self, "get_view_wrapper"): view = self.get_view_wrapper(kwargs)(view) self.definitions.append((method, view, args)) @@ -311,10 +317,10 @@ def add_view(self, method, view, **kwargs): self.defined_methods.append(method) # auto-define a HEAD method if we have a definition for GET. - if method == 'GET': - self.definitions.append(('HEAD', view, args)) - if 'HEAD' not in self.defined_methods: - self.defined_methods.append('HEAD') + if method == "GET": + self.definitions.append(("HEAD", view, args)) + if "HEAD" not in self.defined_methods: + self.defined_methods.append("HEAD") def decorator(self, method, **kwargs): """Add the ability to define methods using python's decorators @@ -327,9 +333,11 @@ def decorator(self, method, **kwargs): def my_view(request): pass """ + def wrapper(view): self.add_view(method, view, **kwargs) return view + return wrapper def get(self, **kwargs): @@ -434,7 +442,7 @@ def get_acceptable(self, method, filter_callables=False): This toggles filtering the callables (default: False) """ - return self.filter_argumentlist(method, 'accept', filter_callables) + return self.filter_argumentlist(method, "accept", filter_callables) def get_contenttypes(self, method, filter_callables=False): """return a list of supported ingress content-type headers that were @@ -447,8 +455,7 @@ def get_contenttypes(self, method, filter_callables=False): This toggles filtering the callables (default: False) """ - return self.filter_argumentlist(method, 'content_type', - filter_callables) + return self.filter_argumentlist(method, "content_type", filter_callables) def get_validators(self, method): """return a list of validators for the given method. @@ -457,8 +464,8 @@ def get_validators(self, method): """ validators = [] for meth, view, args in self.definitions: - if meth.upper() == method.upper() and 'validators' in args: - for validator in args['validators']: + if meth.upper() == method.upper() and "validators" in args: + for validator in args["validators"]: if validator not in validators: validators.append(validator) return validators @@ -482,8 +489,8 @@ def cors_supported_headers_for(self, method=None): """ headers = set() for meth, _, args in self.definitions: - if args.get('cors_enabled', True): - exposed_headers = args.get('cors_headers', ()) + if args.get("cors_enabled", True): + exposed_headers = args.get("cors_headers", ()) if method is not None: if meth.upper() == method.upper(): return set(exposed_headers) @@ -496,15 +503,15 @@ def cors_supported_methods(self): """Return an iterable of methods supported by CORS""" methods = [] for meth, _, args in self.definitions: - if args.get('cors_enabled', True) and meth not in methods: + if args.get("cors_enabled", True) and meth not in methods: methods.append(meth) return methods @property def cors_supported_origins(self): - origins = set(getattr(self, 'cors_origins', ())) + origins = set(getattr(self, "cors_origins", ())) for _, _, args in self.definitions: - origins |= set(args.get('cors_origins', ())) + origins |= set(args.get("cors_origins", ())) return origins def cors_origins_for(self, method): @@ -512,7 +519,7 @@ def cors_origins_for(self, method): origins = set() for meth, view, args in self.definitions: if meth.upper() == method.upper(): - origins |= set(args.get('cors_origins', ())) + origins |= set(args.get("cors_origins", ())) if not origins: origins = self.cors_origins @@ -526,9 +533,9 @@ def cors_support_credentials_for(self, method=None): """ for meth, view, args in self.definitions: if method and meth.upper() == method.upper(): - return args.get('cors_credentials', False) + return args.get("cors_credentials", False) - if getattr(self, 'cors_credentials', False): + if getattr(self, "cors_credentials", False): return self.cors_credentials return False @@ -536,11 +543,11 @@ def cors_max_age_for(self, method=None): max_age = None for meth, view, args in self.definitions: if method and meth.upper() == method.upper(): - max_age = args.get('cors_max_age', None) + max_age = args.get("cors_max_age", None) break if max_age is None: - max_age = getattr(self, 'cors_max_age', None) + max_age = getattr(self, "cors_max_age", None) return max_age @@ -555,19 +562,20 @@ def decorate_view(view, args, method, route_args={}): :param method: the HTTP method :param route_args: the args used for the associated route """ + def wrapper(request): # if the args contain a klass argument then use it to resolve the view # location (if the view argument isn't a callable) ob = None view_ = view - if 'klass' in args and not callable(view): + if "klass" in args and not callable(view): # XXX: given that request.context exists and root-factory # only expects request param, having params seems unnecessary # ob = args['klass'](request) params = dict(request=request) - if 'factory' in route_args: - params['context'] = request.context - ob = args['klass'](**params) + if "factory" in route_args: + params["context"] = request.context + ob = args["klass"](**params) if is_string(view): view_ = getattr(ob, view.lower()) elif isinstance(view, _UnboundView): @@ -576,7 +584,7 @@ def wrapper(request): # the validators can either be a list of callables or contain some # non-callable values. In which case we want to resolve them using the # object if any - validators = args.get('validators', ()) + validators = args.get("validators", ()) for validator in validators: if is_string(validator) and ob is not None: validator = getattr(ob, validator) @@ -592,19 +600,19 @@ def wrapper(request): response = view_(request) except Exception: # cors headers need to be set if an exception was raised - request.info['cors_checked'] = False + request.info["cors_checked"] = False raise # check for errors and return them if any if len(request.errors) > 0: # We already checked for CORS, but since the response is created # again, we want to do that again before returning the response. - request.info['cors_checked'] = False - return args['error_handler'](request) + request.info["cors_checked"] = False + return args["error_handler"](request) # if the view returns its own response, cors headers need to be set if isinstance(response, Response): - request.info['cors_checked'] = False + request.info["cors_checked"] = False # We can't apply filters at this level, since "response" may not have # been rendered into a proper Response object yet. Instead, give the diff --git a/src/cornice/util.py b/src/cornice/util.py index b5e09f72..aadce32c 100644 --- a/src/cornice/util.py +++ b/src/cornice/util.py @@ -3,8 +3,15 @@ # You can obtain one at http://mozilla.org/MPL/2.0/. import warnings -__all__ = ['is_string', 'to_list', 'match_accept_header', - 'ContentTypePredicate', 'current_service', 'func_name'] + +__all__ = [ + "is_string", + "to_list", + "match_accept_header", + "ContentTypePredicate", + "current_service", + "func_name", +] def is_string(s): @@ -14,7 +21,9 @@ def is_string(s): def to_list(obj): """Convert an object to a list if it is not already one""" if not isinstance(obj, (list, tuple)): - obj = [obj, ] + obj = [ + obj, + ] return obj @@ -34,7 +43,7 @@ def match_accept_header(func, context, request): It obtains the request object as single argument. """ acceptable = to_list(func(request)) - request.info['acceptable'] = acceptable + request.info["acceptable"] = acceptable return len(request.accept.acceptable_offers(acceptable)) > 0 @@ -56,20 +65,19 @@ def match_content_type_header(func, context, request): It obtains the request object as single argument. """ supported_contenttypes = to_list(func(request)) - request.info['supported_contenttypes'] = supported_contenttypes + request.info["supported_contenttypes"] = supported_contenttypes return content_type_matches(request, supported_contenttypes) def extract_json_data(request): - warnings.warn("Use ``cornice.validators.extract_cstruct()`` instead", - DeprecationWarning) + warnings.warn("Use ``cornice.validators.extract_cstruct()`` instead", DeprecationWarning) from cornice.validators import extract_cstruct - return extract_cstruct(request)['body'] + + return extract_cstruct(request)["body"] def extract_form_urlencoded_data(request): - warnings.warn("Use ``cornice.validators.extract_cstruct()`` instead", - DeprecationWarning) + warnings.warn("Use ``cornice.validators.extract_cstruct()`` instead", DeprecationWarning) return request.POST @@ -90,11 +98,12 @@ class ContentTypePredicate(object): http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/hooks.html #view-and-route-predicates """ + def __init__(self, val, config): self.val = val def text(self): - return 'content_type = %s' % (self.val,) + return "content_type = %s" % (self.val,) phash = text @@ -106,10 +115,10 @@ def func_name(f): """Return the name of a function or class method.""" if isinstance(f, str): return f - elif hasattr(f, '__qualname__'): # pragma: no cover + elif hasattr(f, "__qualname__"): # pragma: no cover return f.__qualname__ # Python 3 - elif hasattr(f, 'im_class'): # pragma: no cover - return '{0}.{1}'.format(f.im_class.__name__, f.__name__) # Python 2 + elif hasattr(f, "im_class"): # pragma: no cover + return "{0}.{1}".format(f.im_class.__name__, f.__name__) # Python 2 else: # pragma: no cover return f.__name__ # Python 2 @@ -125,5 +134,5 @@ def current_service(request): pattern = request.matched_route.pattern name = request.matched_route.name # try pattern first, then route name else return None - service = services.get(pattern, services.get('__cornice' + name)) + service = services.get(pattern, services.get("__cornice" + name)) return service diff --git a/src/cornice/validators/__init__.py b/src/cornice/validators/__init__.py index a9f110b1..fc66736d 100755 --- a/src/cornice/validators/__init__.py +++ b/src/cornice/validators/__init__.py @@ -4,33 +4,36 @@ import re from webob.multidict import MultiDict -from cornice.validators._colander import ( - validator as colander_validator, - body_validator as colander_body_validator, - headers_validator as colander_headers_validator, - path_validator as colander_path_validator, - querystring_validator as colander_querystring_validator) + +from cornice.validators._colander import body_validator as colander_body_validator +from cornice.validators._colander import headers_validator as colander_headers_validator +from cornice.validators._colander import path_validator as colander_path_validator +from cornice.validators._colander import querystring_validator as colander_querystring_validator +from cornice.validators._colander import validator as colander_validator +from cornice.validators._marshmallow import body_validator as marshmallow_body_validator +from cornice.validators._marshmallow import headers_validator as marshmallow_headers_validator +from cornice.validators._marshmallow import path_validator as marshmallow_path_validator from cornice.validators._marshmallow import ( - validator as marshmallow_validator, - body_validator as marshmallow_body_validator, - headers_validator as marshmallow_headers_validator, - path_validator as marshmallow_path_validator, - querystring_validator as marshmallow_querystring_validator) + querystring_validator as marshmallow_querystring_validator, +) +from cornice.validators._marshmallow import validator as marshmallow_validator -__all__ = ['colander_validator', - 'colander_body_validator', - 'colander_headers_validator', - 'colander_path_validator', - 'colander_querystring_validator', - 'marshmallow_validator', - 'marshmallow_body_validator', - 'marshmallow_headers_validator', - 'marshmallow_path_validator', - 'marshmallow_querystring_validator', - 'extract_cstruct', - 'DEFAULT_VALIDATORS', - 'DEFAULT_FILTERS'] +__all__ = [ + "colander_validator", + "colander_body_validator", + "colander_headers_validator", + "colander_path_validator", + "colander_querystring_validator", + "marshmallow_validator", + "marshmallow_body_validator", + "marshmallow_headers_validator", + "marshmallow_path_validator", + "marshmallow_querystring_validator", + "extract_cstruct", + "DEFAULT_VALIDATORS", + "DEFAULT_FILTERS", +] DEFAULT_VALIDATORS = [] @@ -49,12 +52,9 @@ def extract_cstruct(request): :returns: A mapping containing most request attributes. :rtype: dict """ - is_json = re.match('^application/(.*?)json$', str(request.content_type)) + is_json = re.match("^application/(.*?)json$", str(request.content_type)) - if request.content_type in ( - 'application/x-www-form-urlencoded', - 'multipart/form-data' - ): + if request.content_type in ("application/x-www-form-urlencoded", "multipart/form-data"): body = request.POST.mixed() elif request.content_type and not is_json: body = request.body @@ -63,24 +63,23 @@ def extract_cstruct(request): try: body = request.json_body except ValueError as e: - request.errors.add('body', '', 'Invalid JSON: %s' % e) + request.errors.add("body", "", "Invalid JSON: %s" % e) return {} else: - if not hasattr(body, 'items') and not isinstance(body, list): - request.errors.add('body', '', - 'Should be a JSON object or an array') + if not hasattr(body, "items") and not isinstance(body, list): + request.errors.add("body", "", "Should be a JSON object or an array") return {} else: body = {} - cstruct = {'method': request.method, - 'url': request.url, - 'path': request.matchdict, - 'body': body} + cstruct = { + "method": request.method, + "url": request.url, + "path": request.matchdict, + "body": body, + } - for sub, attr in (('querystring', 'GET'), - ('header', 'headers'), - ('cookies', 'cookies')): + for sub, attr in (("querystring", "GET"), ("header", "headers"), ("cookies", "cookies")): data = getattr(request, attr) if isinstance(data, MultiDict): data = data.mixed() diff --git a/src/cornice/validators/_colander.py b/src/cornice/validators/_colander.py index 61312519..684d4d97 100644 --- a/src/cornice/validators/_colander.py +++ b/src/cornice/validators/_colander.py @@ -17,6 +17,7 @@ def _generate_colander_validator(location): location. :rtype: callable """ + def _validator(request, schema=None, deserializer=None, **kwargs): """ Validate the location against the schema defined on the service. @@ -44,14 +45,14 @@ def _validator(request, schema=None, deserializer=None, **kwargs): schema_instance = _ensure_instantiated(schema) if not isinstance(schema_instance, colander.MappingSchema): - raise TypeError("Schema should inherit from " - "colander.MappingSchema.") + raise TypeError("Schema should inherit from " "colander.MappingSchema.") class RequestSchemaMeta(colander._SchemaMeta): """ A metaclass that will inject a location class attribute into RequestSchema. """ + def __new__(cls, name, bases, class_attrs): """ Instantiate the RequestSchema class. @@ -69,6 +70,7 @@ def __new__(cls, name, bases, class_attrs): class RequestSchema(colander.MappingSchema, metaclass=RequestSchemaMeta): # noqa """A schema to validate the request's location attributes.""" + pass validator(request, RequestSchema(), deserializer, **kwargs) @@ -80,10 +82,10 @@ class RequestSchema(colander.MappingSchema, metaclass=RequestSchemaMeta): # noq return _validator -body_validator = _generate_colander_validator('body') -headers_validator = _generate_colander_validator('headers') -path_validator = _generate_colander_validator('path') -querystring_validator = _generate_colander_validator('querystring') +body_validator = _generate_colander_validator("body") +headers_validator = _generate_colander_validator("headers") +path_validator = _generate_colander_validator("path") +querystring_validator = _generate_colander_validator("querystring") def validator(request, schema=None, deserializer=None, **kwargs): @@ -106,6 +108,7 @@ def validator(request, schema=None, deserializer=None, **kwargs): :func:`cornice.validators.extract_cstruct` """ import colander + from cornice.validators import extract_cstruct if deserializer is None: @@ -122,7 +125,7 @@ def validator(request, schema=None, deserializer=None, **kwargs): translate = request.localizer.translate error_dict = e.asdict(translate=translate) for name, msg in error_dict.items(): - location, _, field = name.partition('.') + location, _, field = name.partition(".") request.errors.add(location, field, msg) else: request.validated.update(deserialized) @@ -131,9 +134,9 @@ def validator(request, schema=None, deserializer=None, **kwargs): def _ensure_instantiated(schema): if inspect.isclass(schema): warnings.warn( - "Setting schema to a class is deprecated. " - " Set schema to an instance instead.", + "Setting schema to a class is deprecated. " " Set schema to an instance instead.", DeprecationWarning, - stacklevel=2) + stacklevel=2, + ) schema = schema() return schema diff --git a/src/cornice/validators/_marshmallow.py b/src/cornice/validators/_marshmallow.py index 6ead720b..2e9ce951 100644 --- a/src/cornice/validators/_marshmallow.py +++ b/src/cornice/validators/_marshmallow.py @@ -46,12 +46,12 @@ def _validator(request, schema=None, deserializer=None, **kwargs): return # see if the user wants to set any keyword arguments for their schema - schema_kwargs = kwargs.get('schema_kwargs', {}) + schema_kwargs = kwargs.get("schema_kwargs", {}) schema = _instantiate_schema(schema, **schema_kwargs) class ValidatedField(marshmallow.fields.Field): def _deserialize(self, value, attr, data, **kwargs): - schema.context.setdefault('request', request) + schema.context.setdefault("request", request) deserialized = schema.load(value) return deserialized @@ -80,12 +80,14 @@ def __new__(cls, name, bases, class_attrs): """ class_attrs[location] = ValidatedField( - required=True, metadata={"load_from": location}) - class_attrs['Meta'] = Meta + required=True, metadata={"load_from": location} + ) + class_attrs["Meta"] = Meta return type(name, bases, class_attrs) class RequestSchema(marshmallow.Schema, metaclass=RequestSchemaMeta): # noqa """A schema to validate the request's location attributes.""" + pass validator(request, RequestSchema, deserializer, **kwargs) @@ -94,10 +96,10 @@ class RequestSchema(marshmallow.Schema, metaclass=RequestSchemaMeta): # noqa return _validator -body_validator = _generate_marshmallow_validator('body') -headers_validator = _generate_marshmallow_validator('header') -path_validator = _generate_marshmallow_validator('path') -querystring_validator = _generate_marshmallow_validator('querystring') +body_validator = _generate_marshmallow_validator("body") +headers_validator = _generate_marshmallow_validator("header") +path_validator = _generate_marshmallow_validator("path") +querystring_validator = _generate_marshmallow_validator("querystring") def _message_normalizer(exc, no_field_name="_schema"): @@ -110,7 +112,7 @@ def _message_normalizer(exc, no_field_name="_schema"): """ if isinstance(exc.messages, dict): return exc.messages - field_names = exc.kwargs.get('field_names', []) + field_names = exc.kwargs.get("field_names", []) if len(field_names) == 0: return {no_field_name: exc.messages} return dict((name, exc.messages) for name in field_names) @@ -136,6 +138,7 @@ def validator(request, schema=None, deserializer=None, **kwargs): :func:`cornice.validators.extract_cstruct` """ import marshmallow + from cornice.validators import extract_cstruct if deserializer is None: @@ -145,7 +148,7 @@ def validator(request, schema=None, deserializer=None, **kwargs): return schema = _instantiate_schema(schema) - schema.context.setdefault('request', request) + schema.context.setdefault("request", request) cstruct = deserializer(request) try: @@ -154,8 +157,8 @@ def validator(request, schema=None, deserializer=None, **kwargs): # translate = request.localizer.translate normalized_errors = _message_normalizer(err) for location, details in normalized_errors.items(): - location = location if location != '_schema' else '' - if hasattr(details, 'items'): + location = location if location != "_schema" else "" + if hasattr(details, "items"): for subfield, msg in details.items(): request.errors.add(location, subfield, msg) else: @@ -175,6 +178,5 @@ def _instantiate_schema(schema, **kwargs): :return: The object of the marshmallow schema """ if not inspect.isclass(schema): - raise ValueError('You need to pass Marshmallow class instead ' - 'of schema instance') + raise ValueError("You need to pass Marshmallow class instead " "of schema instance") return schema(**kwargs) diff --git a/tests/support.py b/tests/support.py index aec7c593..e6a054e0 100644 --- a/tests/support.py +++ b/tests/support.py @@ -5,27 +5,26 @@ import logging.handlers import weakref + try: from unittest2 import TestCase except ImportError: # Maybe we're running in python2.7? from unittest import TestCase # NOQA -from webob.dec import wsgify -from webob import exc -from pyramid.httpexceptions import HTTPException -from pyramid import testing - from cornice.errors import Errors +from pyramid import testing +from pyramid.httpexceptions import HTTPException +from webob import exc +from webob.dec import wsgify -logger = logging.getLogger('cornice') +logger = logging.getLogger("cornice") class DummyContext(object): - def __repr__(self): - return 'context!' + return "context!" class DummyRequest(testing.DummyRequest): @@ -96,8 +95,7 @@ def get_logs(self, level=logging.WARNING, flush=True): called, unless *flush* is False (this is useful to get e.g. all warnings then all info messages). """ - messages = [log.getMessage() for log in self.loghandler.buffer - if log.levelno == level] + messages = [log.getMessage() for log in self.loghandler.buffer if log.levelno == level] if flush: self.loghandler.flush() return messages @@ -106,7 +104,7 @@ def get_logs(self, level=logging.WARNING, flush=True): class CatchErrors(object): def __init__(self, app): self.app = app - if hasattr(app, 'registry'): + if hasattr(app, "registry"): self.registry = app.registry @wsgify diff --git a/tests/test_cors.py b/tests/test_cors.py index 1980f550..12281fef 100644 --- a/tests/test_cors.py +++ b/tests/test_cors.py @@ -1,53 +1,49 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. +from cornice.service import Service from pyramid import testing from pyramid.authentication import BasicAuthAuthenticationPolicy -from pyramid.exceptions import NotFound, HTTPBadRequest +from pyramid.exceptions import HTTPBadRequest, NotFound from pyramid.interfaces import IAuthorizationPolicy from pyramid.response import Response from pyramid.view import view_config -from zope.interface import implementer - from webtest import TestApp +from zope.interface import implementer -from cornice.service import Service - -from .support import TestCase, CatchErrors +from .support import CatchErrors, TestCase -squirel = Service(path='/squirel', name='squirel', cors_origins=('foobar',)) -spam = Service(path='/spam', name='spam', cors_origins=('*',)) -eggs = Service(path='/eggs', name='egg', cors_origins=('*',), - cors_expose_all_headers=False) -bacon = Service(path='/bacon/{type}', name='bacon', cors_origins=('*',)) +squirel = Service(path="/squirel", name="squirel", cors_origins=("foobar",)) +spam = Service(path="/spam", name="spam", cors_origins=("*",)) +eggs = Service(path="/eggs", name="egg", cors_origins=("*",), cors_expose_all_headers=False) +bacon = Service(path="/bacon/{type}", name="bacon", cors_origins=("*",)) class Klass(object): """ Class implementation of a service """ + def __init__(self, request): self.request = request def post(self): return "moar squirels (take care)" -cors_policy = {'origins': ('*',), 'enabled': True, 'credentials': True} -cors_klass = Service(name='cors_klass', - path='/cors_klass', - klass=Klass, - cors_policy=cors_policy) -cors_klass.add_view('post', 'post') +cors_policy = {"origins": ("*",), "enabled": True, "credentials": True} + +cors_klass = Service(name="cors_klass", path="/cors_klass", klass=Klass, cors_policy=cors_policy) +cors_klass.add_view("post", "post") -@squirel.get(cors_origins=('notmyidea.org',), cors_headers=('X-My-Header',)) +@squirel.get(cors_origins=("notmyidea.org",), cors_headers=("X-My-Header",)) def get_squirel(request): return "squirels" -@squirel.post(cors_enabled=False, cors_headers=('X-Another-Header')) +@squirel.post(cors_enabled=False, cors_headers=("X-Another-Header")) def post_squirel(request): return "moar squirels (take care)" @@ -57,33 +53,34 @@ def put_squirel(request): return "squirels!" -@spam.get(cors_credentials=True, cors_headers=('X-My-Header'), - cors_max_age=42) +@spam.get(cors_credentials=True, cors_headers=("X-My-Header"), cors_max_age=42) def gimme_some_spam_please(request): - return 'spam' + return "spam" -@spam.post(permission='read-only') +@spam.post(permission="read-only") def moar_spam(request): - return 'moar spam' + return "moar spam" -@eggs.get(cors_origins=('notmyidea.org',), - cors_headers=('X-My-Header', 'X-Another-Header', 'X-Another-Header2')) +@eggs.get( + cors_origins=("notmyidea.org",), + cors_headers=("X-My-Header", "X-Another-Header", "X-Another-Header2"), +) def get_eggs(request): return "eggs" def is_bacon_good(request, **kwargs): - if not request.matchdict['type'].endswith('good'): - request.errors.add('querystring', 'type', 'should be better!') + if not request.matchdict["type"].endswith("good"): + request.errors.add("querystring", "type", "should be better!") @bacon.get(validators=is_bacon_good) def get_some_bacon(request): # Okay, you there. Bear in mind, the only kind of bacon existing is 'good'. - if request.matchdict['type'] != 'good': - raise NotFound(detail='Not. Found.') + if request.matchdict["type"] != "good": + raise NotFound(detail="Not. Found.") return "yay" @@ -97,248 +94,238 @@ def put_some_bacon(request): raise HTTPBadRequest() -@view_config(route_name='noservice') +@view_config(route_name="noservice") def noservice(request): - return Response('No Service here.') + return Response("No Service here.") class TestCORS(TestCase): - def setUp(self): self.config = testing.setUp() - self.config.include('cornice') - self.config.add_route('noservice', '/noservice') - self.config.scan('tests.test_cors') + self.config.include("cornice") + self.config.add_route("noservice", "/noservice") + self.config.scan("tests.test_cors") self.app = TestApp(CatchErrors(self.config.make_wsgi_app())) def tearDown(self): testing.tearDown() def test_preflight_cors_klass_post(self): - resp = self.app.options('/cors_klass', - status=200, - headers={ - 'Origin': 'lolnet.org', - 'Access-Control-Request-Method': 'POST'}) - self.assertEqual('POST,OPTIONS', - dict(resp.headers)['Access-Control-Allow-Methods']) + resp = self.app.options( + "/cors_klass", + status=200, + headers={"Origin": "lolnet.org", "Access-Control-Request-Method": "POST"}, + ) + self.assertEqual("POST,OPTIONS", dict(resp.headers)["Access-Control-Allow-Methods"]) def test_preflight_cors_klass_put(self): - self.app.options('/cors_klass', - status=400, - headers={ - 'Origin': 'lolnet.org', - 'Access-Control-Request-Method': 'PUT'}) + self.app.options( + "/cors_klass", + status=400, + headers={"Origin": "lolnet.org", "Access-Control-Request-Method": "PUT"}, + ) def test_preflight_missing_headers(self): # we should have an OPTION method defined. # If we just try to reach it, without using correct headers: # "Access-Control-Request-Method"or without the "Origin" header, # we should get a 400. - resp = self.app.options('/squirel', status=400) - self.assertEqual(len(resp.json['errors']), 2) + resp = self.app.options("/squirel", status=400) + self.assertEqual(len(resp.json["errors"]), 2) def test_preflight_missing_origin(self): - resp = self.app.options( - '/squirel', - headers={'Access-Control-Request-Method': 'GET'}, - status=400) - self.assertEqual(len(resp.json['errors']), 1) + "/squirel", headers={"Access-Control-Request-Method": "GET"}, status=400 + ) + self.assertEqual(len(resp.json["errors"]), 1) def test_preflight_does_not_expose_headers(self): resp = self.app.options( - '/squirel', - headers={'Access-Control-Request-Method': 'GET', - 'Origin': 'notmyidea.org'}, - status=200) - self.assertNotIn('Access-Control-Expose-Headers', resp.headers) + "/squirel", + headers={"Access-Control-Request-Method": "GET", "Origin": "notmyidea.org"}, + status=200, + ) + self.assertNotIn("Access-Control-Expose-Headers", resp.headers) def test_preflight_missing_request_method(self): + resp = self.app.options("/squirel", headers={"Origin": "foobar.org"}, status=400) - resp = self.app.options( - '/squirel', - headers={'Origin': 'foobar.org'}, - status=400) - - self.assertEqual(len(resp.json['errors']), 1) + self.assertEqual(len(resp.json["errors"]), 1) def test_preflight_incorrect_origin(self): # we put "lolnet.org" where only "notmyidea.org" is authorized resp = self.app.options( - '/squirel', - headers={'Origin': 'lolnet.org', - 'Access-Control-Request-Method': 'GET'}, - status=400) - self.assertEqual(len(resp.json['errors']), 1) + "/squirel", + headers={"Origin": "lolnet.org", "Access-Control-Request-Method": "GET"}, + status=400, + ) + self.assertEqual(len(resp.json["errors"]), 1) def test_preflight_correct_origin(self): resp = self.app.options( - '/squirel', - headers={'Origin': 'notmyidea.org', - 'Access-Control-Request-Method': 'GET'}) - self.assertEqual( - resp.headers['Access-Control-Allow-Origin'], - 'notmyidea.org') + "/squirel", headers={"Origin": "notmyidea.org", "Access-Control-Request-Method": "GET"} + ) + self.assertEqual(resp.headers["Access-Control-Allow-Origin"], "notmyidea.org") - allowed_methods = (resp.headers['Access-Control-Allow-Methods'] - .split(',')) + allowed_methods = resp.headers["Access-Control-Allow-Methods"].split(",") - self.assertNotIn('POST', allowed_methods) - self.assertIn('GET', allowed_methods) - self.assertIn('PUT', allowed_methods) - self.assertIn('HEAD', allowed_methods) + self.assertNotIn("POST", allowed_methods) + self.assertIn("GET", allowed_methods) + self.assertIn("PUT", allowed_methods) + self.assertIn("HEAD", allowed_methods) - allowed_headers = (resp.headers['Access-Control-Allow-Headers'] - .split(',')) + allowed_headers = resp.headers["Access-Control-Allow-Headers"].split(",") - self.assertIn('X-My-Header', allowed_headers) - self.assertNotIn('X-Another-Header', allowed_headers) + self.assertIn("X-My-Header", allowed_headers) + self.assertNotIn("X-Another-Header", allowed_headers) def test_preflight_deactivated_method(self): - self.app.options('/squirel', - headers={'Origin': 'notmyidea.org', - 'Access-Control-Request-Method': 'POST'}, - status=400) + self.app.options( + "/squirel", + headers={"Origin": "notmyidea.org", "Access-Control-Request-Method": "POST"}, + status=400, + ) def test_preflight_origin_not_allowed_for_method(self): - self.app.options('/squirel', - headers={'Origin': 'notmyidea.org', - 'Access-Control-Request-Method': 'PUT'}, - status=400) + self.app.options( + "/squirel", + headers={"Origin": "notmyidea.org", "Access-Control-Request-Method": "PUT"}, + status=400, + ) def test_preflight_credentials_are_supported(self): resp = self.app.options( - '/spam', headers={'Origin': 'notmyidea.org', - 'Access-Control-Request-Method': 'GET'}) - self.assertIn('Access-Control-Allow-Credentials', resp.headers) - self.assertEqual(resp.headers['Access-Control-Allow-Credentials'], - 'true') + "/spam", headers={"Origin": "notmyidea.org", "Access-Control-Request-Method": "GET"} + ) + self.assertIn("Access-Control-Allow-Credentials", resp.headers) + self.assertEqual(resp.headers["Access-Control-Allow-Credentials"], "true") def test_preflight_credentials_header_not_included_when_not_needed(self): resp = self.app.options( - '/spam', headers={'Origin': 'notmyidea.org', - 'Access-Control-Request-Method': 'POST'}) + "/spam", headers={"Origin": "notmyidea.org", "Access-Control-Request-Method": "POST"} + ) - self.assertNotIn('Access-Control-Allow-Credentials', resp.headers) + self.assertNotIn("Access-Control-Allow-Credentials", resp.headers) def test_preflight_contains_max_age(self): resp = self.app.options( - '/spam', headers={'Origin': 'notmyidea.org', - 'Access-Control-Request-Method': 'GET'}) + "/spam", headers={"Origin": "notmyidea.org", "Access-Control-Request-Method": "GET"} + ) - self.assertIn('Access-Control-Max-Age', resp.headers) - self.assertEqual(resp.headers['Access-Control-Max-Age'], '42') + self.assertIn("Access-Control-Max-Age", resp.headers) + self.assertEqual(resp.headers["Access-Control-Max-Age"], "42") def test_resp_dont_include_allow_origin(self): - resp = self.app.get('/squirel') # omit the Origin header - self.assertNotIn('Access-Control-Allow-Origin', resp.headers) - self.assertEqual(resp.json, 'squirels') + resp = self.app.get("/squirel") # omit the Origin header + self.assertNotIn("Access-Control-Allow-Origin", resp.headers) + self.assertEqual(resp.json, "squirels") def test_origin_is_not_wildcard_if_allow_credentials(self): resp = self.app.options( - '/cors_klass', + "/cors_klass", status=200, headers={ - 'Origin': 'lolnet.org', - 'Access-Control-Request-Method': 'POST', - }) - self.assertEqual(resp.headers['Access-Control-Allow-Origin'], - 'lolnet.org') - self.assertEqual(resp.headers['Access-Control-Allow-Credentials'], - 'true') + "Origin": "lolnet.org", + "Access-Control-Request-Method": "POST", + }, + ) + self.assertEqual(resp.headers["Access-Control-Allow-Origin"], "lolnet.org") + self.assertEqual(resp.headers["Access-Control-Allow-Credentials"], "true") def test_responses_include_an_allow_origin_header(self): - resp = self.app.get('/squirel', headers={'Origin': 'notmyidea.org'}) - self.assertIn('Access-Control-Allow-Origin', resp.headers) - self.assertEqual(resp.headers['Access-Control-Allow-Origin'], - 'notmyidea.org') + resp = self.app.get("/squirel", headers={"Origin": "notmyidea.org"}) + self.assertIn("Access-Control-Allow-Origin", resp.headers) + self.assertEqual(resp.headers["Access-Control-Allow-Origin"], "notmyidea.org") def test_credentials_are_included(self): - resp = self.app.get('/spam', headers={'Origin': 'notmyidea.org'}) - self.assertIn('Access-Control-Allow-Credentials', resp.headers) - self.assertEqual(resp.headers['Access-Control-Allow-Credentials'], - 'true') + resp = self.app.get("/spam", headers={"Origin": "notmyidea.org"}) + self.assertIn("Access-Control-Allow-Credentials", resp.headers) + self.assertEqual(resp.headers["Access-Control-Allow-Credentials"], "true") def test_headers_are_exposed(self): - resp = self.app.get('/squirel', headers={'Origin': 'notmyidea.org'}) - self.assertIn('Access-Control-Expose-Headers', resp.headers) + resp = self.app.get("/squirel", headers={"Origin": "notmyidea.org"}) + self.assertIn("Access-Control-Expose-Headers", resp.headers) - headers = resp.headers['Access-Control-Expose-Headers'].split(',') - self.assertIn('X-My-Header', headers) + headers = resp.headers["Access-Control-Expose-Headers"].split(",") + self.assertIn("X-My-Header", headers) def test_preflight_request_headers_are_included(self): resp = self.app.options( - '/squirel', headers={ - 'Origin': 'notmyidea.org', - 'Access-Control-Request-Method': 'GET', - 'Access-Control-Request-Headers': 'foo, bar,baz '}) + "/squirel", + headers={ + "Origin": "notmyidea.org", + "Access-Control-Request-Method": "GET", + "Access-Control-Request-Headers": "foo, bar,baz ", + }, + ) # The specification says we can have any number of LWS (Linear white # spaces) in the values and that it should be removed. # per default, they should be authorized, and returned in the list of # authorized headers - headers = resp.headers['Access-Control-Allow-Headers'].split(',') - self.assertIn('foo', headers) - self.assertIn('bar', headers) - self.assertIn('baz', headers) + headers = resp.headers["Access-Control-Allow-Headers"].split(",") + self.assertIn("foo", headers) + self.assertIn("bar", headers) + self.assertIn("baz", headers) def test_preflight_request_headers_isnt_too_permissive(self): # The specification says we can have any number of LWS (Linear white # spaces) in the values. self.app.options( - '/eggs', headers={ - 'Origin': 'notmyidea.org', - 'Access-Control-Request-Method': 'GET', - 'Access-Control-Request-Headers': ( - ' X-My-Header ,X-Another-Header, X-Another-Header2 ' - )}, - status=200) + "/eggs", + headers={ + "Origin": "notmyidea.org", + "Access-Control-Request-Method": "GET", + "Access-Control-Request-Headers": ( + " X-My-Header ,X-Another-Header, X-Another-Header2 " + ), + }, + status=200, + ) self.app.options( - '/eggs', headers={ - 'Origin': 'notmyidea.org', - 'Access-Control-Request-Method': 'GET', - 'Access-Control-Request-Headers': ( - 'X-My-Header ,baz , X-Another-Header ' - )}, - status=400) + "/eggs", + headers={ + "Origin": "notmyidea.org", + "Access-Control-Request-Method": "GET", + "Access-Control-Request-Headers": ("X-My-Header ,baz , X-Another-Header "), + }, + status=400, + ) def test_preflight_headers_arent_case_sensitive(self): - self.app.options('/spam', headers={ - 'Origin': 'notmyidea.org', - 'Access-Control-Request-Method': 'GET', - 'Access-Control-Request-Headers': 'x-my-header', }) + self.app.options( + "/spam", + headers={ + "Origin": "notmyidea.org", + "Access-Control-Request-Method": "GET", + "Access-Control-Request-Headers": "x-my-header", + }, + ) def test_400_returns_CORS_headers(self): - resp = self.app.get('/bacon/not', status=400, - headers={'Origin': 'notmyidea.org'}) - self.assertIn('Access-Control-Allow-Origin', resp.headers) + resp = self.app.get("/bacon/not", status=400, headers={"Origin": "notmyidea.org"}) + self.assertIn("Access-Control-Allow-Origin", resp.headers) def test_404_returns_CORS_headers(self): - resp = self.app.get('/bacon/notgood', status=404, - headers={'Origin': 'notmyidea.org'}) - self.assertIn('Access-Control-Allow-Origin', resp.headers) + resp = self.app.get("/bacon/notgood", status=404, headers={"Origin": "notmyidea.org"}) + self.assertIn("Access-Control-Allow-Origin", resp.headers) def test_response_returns_CORS_headers(self): - resp = self.app.post('/bacon/response', status=200, - headers={'Origin': 'notmyidea.org'}) - self.assertIn('Access-Control-Allow-Origin', resp.headers) + resp = self.app.post("/bacon/response", status=200, headers={"Origin": "notmyidea.org"}) + self.assertIn("Access-Control-Allow-Origin", resp.headers) def test_raise_returns_CORS_headers(self): - resp = self.app.put('/bacon/raise', status=400, - headers={'Origin': 'notmyidea.org'}) - self.assertIn('Access-Control-Allow-Origin', resp.headers) + resp = self.app.put("/bacon/raise", status=400, headers={"Origin": "notmyidea.org"}) + self.assertIn("Access-Control-Allow-Origin", resp.headers) def test_existing_non_service_route(self): - resp = self.app.get('/noservice', status=200, - headers={'Origin': 'notmyidea.org'}) - self.assertEqual(resp.body, b'No Service here.') + resp = self.app.get("/noservice", status=200, headers={"Origin": "notmyidea.org"}) + self.assertEqual(resp.body, b"No Service here.") class TestAuthenticatedCORS(TestCase): def setUp(self): - def check_cred(username, *args, **kwargs): return [username] @@ -348,56 +335,55 @@ def permits(self, context, principals, permission): return permission in principals self.config = testing.setUp() - self.config.include('cornice') - self.config.add_route('noservice', '/noservice') + self.config.include("cornice") + self.config.add_route("noservice", "/noservice") self.config.set_authorization_policy(AuthorizationPolicy()) - self.config.set_authentication_policy(BasicAuthAuthenticationPolicy( - check_cred)) - self.config.set_default_permission('readwrite') - self.config.scan('tests.test_cors') + self.config.set_authentication_policy(BasicAuthAuthenticationPolicy(check_cred)) + self.config.set_default_permission("readwrite") + self.config.scan("tests.test_cors") self.app = TestApp(CatchErrors(self.config.make_wsgi_app())) def tearDown(self): testing.tearDown() def test_post_on_spam_should_be_forbidden(self): - self.app.post('/spam', status=403) + self.app.post("/spam", status=403) def test_preflight_does_not_need_authentication(self): - self.app.options('/spam', status=200, - headers={'Origin': 'notmyidea.org', - 'Access-Control-Request-Method': 'POST'}) + self.app.options( + "/spam", + status=200, + headers={"Origin": "notmyidea.org", "Access-Control-Request-Method": "POST"}, + ) class TestAlwaysCORS(TestCase): - def setUp(self): self.config = testing.setUp() - self.config.include('cornice') - self.config.add_settings({ "cornice.always_cors": True }) - self.config.add_route('noservice', '/noservice') - self.config.scan('tests.test_cors') + self.config.include("cornice") + self.config.add_settings({"cornice.always_cors": True}) + self.config.add_route("noservice", "/noservice") + self.config.scan("tests.test_cors") self.app = TestApp(CatchErrors(self.config.make_wsgi_app())) def tearDown(self): testing.tearDown() def test_response_returns_CORS_headers_without_origin(self): - resp = self.app.post('/bacon/response', status=200) - self.assertIn('Access-Control-Allow-Origin', resp.headers) + resp = self.app.post("/bacon/response", status=200) + self.assertIn("Access-Control-Allow-Origin", resp.headers) def test_response_does_not_return_CORS_headers_if_no_origin(self): - resp = self.app.put('/squirel') - self.assertNotIn('Access-Control-Allow-Origin', resp.headers) + resp = self.app.put("/squirel") + self.assertNotIn("Access-Control-Allow-Origin", resp.headers) def test_preflight_checks_origin_when_not_star(self): - self.app.options('/squirel', - headers={'Origin': 'notmyidea.org', - 'Access-Control-Request-Method': 'PUT'}, - status=400) - self.app.put('/squirel', - headers={'Origin': 'notmyidea.org'}, - status=400) + self.app.options( + "/squirel", + headers={"Origin": "notmyidea.org", "Access-Control-Request-Method": "PUT"}, + status=400, + ) + self.app.put("/squirel", headers={"Origin": "notmyidea.org"}, status=400) def test_checks_origin_when_not_star(self): - self.app.put('/squirel', headers={'Origin': 'not foobar'}, status=400) + self.app.put("/squirel", headers={"Origin": "not foobar"}, status=400) diff --git a/tests/test_errors.py b/tests/test_errors.py index 527b3809..93b8d641 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -6,7 +6,7 @@ from pyramid.i18n import TranslationString from webtest import TestApp -from .support import TestCase, CatchErrors +from .support import CatchErrors, TestCase class TestErrorsHelper(TestCase): @@ -14,42 +14,43 @@ def setUp(self): self.errors = Errors() def test_add_to_supported_location(self): - self.errors.add('') - self.errors.add('body', description='!') - self.errors.add('querystring', name='field') - self.errors.add('url') - self.errors.add('header') - self.errors.add('path') - self.errors.add('cookies') - self.errors.add('method') + self.errors.add("") + self.errors.add("body", description="!") + self.errors.add("querystring", name="field") + self.errors.add("url") + self.errors.add("header") + self.errors.add("path") + self.errors.add("cookies") + self.errors.add("method") self.assertEqual(len(self.errors), 8) def test_raises_an_exception_when_location_is_unsupported(self): with self.assertRaises(ValueError): - self.errors.add('something') + self.errors.add("something") service1 = Service(name="service1", path="/error-service1") + @service1.get() def get1(request): - return request.errors.add('body', 'field', 'Description') + return request.errors.add("body", "field", "Description") service2 = Service(name="service2", path="/error-service2") + @service2.get() def get2(request): - return request.errors.add('body', 'field', TranslationString('Description')) + return request.errors.add("body", "field", TranslationString("Description")) class TestErrorsTranslation(TestCase): - def setUp(self): self.config = testing.setUp() self.config.add_settings({"available_languages": "en fr"}) - self.config.include('cornice') - self.config.scan('tests.test_errors') + self.config.include("cornice") + self.config.scan("tests.test_errors") self.app = TestApp(CatchErrors(self.config.make_wsgi_app())) def tearDown(self): @@ -61,10 +62,10 @@ def _translate(self): def test_error_description_translation_not_called_when_string(self): with mock.patch(self._translate) as mocked: - resp = self.app.get('/error-service1', status=400).json + self.app.get("/error-service1", status=400).json self.assertFalse(mocked.called) def test_error_description_translation_called_when_translationstring(self): with mock.patch(self._translate, return_value="Translated") as mocked: - resp = self.app.get('/error-service2', status=400).json + self.app.get("/error-service2", status=400).json self.assertTrue(mocked.called) diff --git a/tests/test_imperative_resource.py b/tests/test_imperative_resource.py index 282af698..d6db0cb7 100644 --- a/tests/test_imperative_resource.py +++ b/tests/test_imperative_resource.py @@ -5,29 +5,25 @@ from unittest import mock import pytest +from cornice.resource import add_resource, add_view from pyramid import testing from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy +from pyramid.httpexceptions import HTTPForbidden, HTTPOk from pyramid.security import Allow -from pyramid.httpexceptions import ( - HTTPOk, HTTPForbidden -) from webtest import TestApp -from cornice.resource import add_resource, add_view - -from .support import TestCase, CatchErrors, dummy_factory +from .support import CatchErrors, TestCase, dummy_factory -USERS = {1: {'name': 'gawel'}, 2: {'name': 'tarek'}} +USERS = {1: {"name": "gawel"}, 2: {"name": "tarek"}} def my_collection_acl(request): - return [(Allow, 'alice', 'read')] + return [(Allow, "alice", "read")] class ThingImp(object): - def __init__(self, request, context=None): self.request = request self.context = context @@ -36,73 +32,79 @@ def __acl__(self): return my_collection_acl(self.request) def collection_get(self): - return 'yay' + return "yay" class UserImp(object): - def __init__(self, request, context=None): self.request = request self.context = context def collection_get(self): - return {'users': list(USERS.keys())} + return {"users": list(USERS.keys())} def get(self): - return USERS.get(int(self.request.matchdict['id'])) + return USERS.get(int(self.request.matchdict["id"])) def collection_post(self): - return {'test': 'yeah'} + return {"test": "yeah"} def patch(self): - return {'test': 'yeah'} + return {"test": "yeah"} def collection_patch(self): - return {'test': 'yeah'} + return {"test": "yeah"} def put(self): return dict(type=repr(self.context)) class TestResourceWarning(TestCase): - @mock.patch('warnings.warn') + @mock.patch("warnings.warn") def test_path_clash(self, mocked_warn): class BadThingImp(object): def __init__(self, request, context=None): pass - add_resource(BadThingImp, collection_path='/badthing/{id}', - path='/badthing/{id}', - name='bad_thing_service') + + add_resource( + BadThingImp, + collection_path="/badthing/{id}", + path="/badthing/{id}", + name="bad_thing_service", + ) msg = "Warning: collection_path and path are not distinct." mocked_warn.assert_called_with(msg) class TestResource(TestCase): - def setUp(self): from pyramid.renderers import JSONP self.config = testing.setUp() - self.config.add_renderer('jsonp', JSONP(param_name='callback')) + self.config.add_renderer("jsonp", JSONP(param_name="callback")) self.config.include("cornice") self.authz_policy = ACLAuthorizationPolicy() self.config.set_authorization_policy(self.authz_policy) - self.authn_policy = AuthTktAuthenticationPolicy('$3kr1t') + self.authn_policy = AuthTktAuthenticationPolicy("$3kr1t") self.config.set_authentication_policy(self.authn_policy) - add_view(ThingImp.collection_get, permission='read') + add_view(ThingImp.collection_get, permission="read") thing_resource = add_resource( - ThingImp, collection_path='/thing', path='/thing/{id}', - name='thing_service') + ThingImp, collection_path="/thing", path="/thing/{id}", name="thing_service" + ) - add_view(UserImp.get, renderer='json') - add_view(UserImp.get, renderer='jsonp', accept='application/javascript') - add_view(UserImp.collection_post, renderer='json', accept='application/json') + add_view(UserImp.get, renderer="json") + add_view(UserImp.get, renderer="jsonp", accept="application/javascript") + add_view(UserImp.collection_post, renderer="json", accept="application/json") user_resource = add_resource( - UserImp, collection_path='/users', path='/users/{id}', - name='user_service', factory=dummy_factory) + UserImp, + collection_path="/users", + path="/users/{id}", + name="user_service", + factory=dummy_factory, + ) self.config.add_cornice_resource(thing_resource) self.config.add_cornice_resource(user_resource) @@ -112,9 +114,9 @@ def tearDown(self): testing.tearDown() def test_basic_resource(self): - self.assertEqual(self.app.get("/users").json, {'users': [1, 2]}) + self.assertEqual(self.app.get("/users").json, {"users": [1, 2]}) - self.assertEqual(self.app.get("/users/1").json, {'name': 'gawel'}) + self.assertEqual(self.app.get("/users/1").json, {"name": "gawel"}) resp = self.app.get("/users/1?callback=test") @@ -124,50 +126,52 @@ def test_accept_headers(self): # the accept headers should work even in case they're specified in a # resource method self.assertEqual( - self.app.post("/users", headers={'Accept': 'application/json'}, - params=json.dumps({'test': 'yeah'})).json, - {'test': 'yeah'}) + self.app.post( + "/users", + headers={"Accept": "application/json"}, + params=json.dumps({"test": "yeah"}), + ).json, + {"test": "yeah"}, + ) def patch(self, *args, **kwargs): - return self.app._gen_request('PATCH', *args, **kwargs) + return self.app._gen_request("PATCH", *args, **kwargs) def test_head_and_patch(self): self.app.head("/users") self.app.head("/users/1") - self.assertEqual( - self.patch("/users").json, - {'test': 'yeah'}) + self.assertEqual(self.patch("/users").json, {"test": "yeah"}) - self.assertEqual( - self.patch("/users/1").json, - {'test': 'yeah'}) + self.assertEqual(self.patch("/users/1").json, {"test": "yeah"}) def test_context_factory(self): - self.assertEqual(self.app.put('/users/1').json, {'type': 'context!'}) + self.assertEqual(self.app.put("/users/1").json, {"type": "context!"}) def test_explicit_collection_service_name(self): route_url = testing.DummyRequest().route_url # service must exist - self.assertTrue(route_url('collection_user_service')) + self.assertTrue(route_url("collection_user_service")) def test_explicit_service_name(self): route_url = testing.DummyRequest().route_url - self.assertTrue(route_url('user_service', id=42)) # service must exist + self.assertTrue(route_url("user_service", id=42)) # service must exist def test_acl_support_unauthenticated_thing_get(self): # calling a view with permissions without an auth'd user => 403 - self.app.get('/thing', status=HTTPForbidden.code) + self.app.get("/thing", status=HTTPForbidden.code) def test_acl_support_unauthenticated_forbidden_thing_get(self): # calling a view with permissions without an auth'd user => 403 - with mock.patch.object(self.authn_policy, 'authenticated_userid', return_value=None): - result = self.app.get('/thing', status=HTTPForbidden.code) + with mock.patch.object(self.authn_policy, "authenticated_userid", return_value=None): + self.app.get("/thing", status=HTTPForbidden.code) def test_acl_support_authenticated_allowed_thing_get(self): - with mock.patch.object(self.authn_policy, 'unauthenticated_userid', return_value='alice'): - with mock.patch.object(self.authn_policy, 'authenticated_userid', return_value='alice'): - result = self.app.get('/thing', status=HTTPOk.code) + with mock.patch.object(self.authn_policy, "unauthenticated_userid", return_value="alice"): + with mock.patch.object( + self.authn_policy, "authenticated_userid", return_value="alice" + ): + result = self.app.get("/thing", status=HTTPOk.code) self.assertEqual("yay", result.json) @@ -180,17 +184,22 @@ class NonAutocommittingConfigurationTestResource(TestCase): def setUp(self): from pyramid.renderers import JSONP + self.config = testing.setUp(autocommit=False) - self.config.add_renderer('jsonp', JSONP(param_name='callback')) + self.config.add_renderer("jsonp", JSONP(param_name="callback")) self.config.include("cornice") - add_view(UserImp.get, renderer='json') + add_view(UserImp.get, renderer="json") # pyramid does not allow having 2 views with same request conditions - add_view(UserImp.get, renderer='jsonp', accept='application/javascript') - add_view(UserImp.collection_post, renderer='json', accept='application/json') + add_view(UserImp.get, renderer="jsonp", accept="application/javascript") + add_view(UserImp.collection_post, renderer="json", accept="application/json") user_resource = add_resource( - UserImp, collection_path='/users', path='/users/{id}', - name='user_service', factory=dummy_factory) + UserImp, + collection_path="/users", + path="/users/{id}", + name="user_service", + factory=dummy_factory, + ) self.config.add_cornice_resource(user_resource) self.app = TestApp(CatchErrors(self.config.make_wsgi_app())) @@ -199,4 +208,4 @@ def tearDown(self): testing.tearDown() def test_get(self): - self.app.get('/users/1') + self.app.get("/users/1") diff --git a/tests/test_init.py b/tests/test_init.py index 2c8e70f1..f3304336 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -3,50 +3,40 @@ # You can obtain one at http://mozilla.org/MPL/2.0/. from unittest import mock +from cornice import Service +from cornice.pyramidhook import apply_filters from pyramid import testing from webtest import TestApp -from cornice import Service -from cornice.pyramidhook import apply_filters -from .support import TestCase, CatchErrors +from .support import CatchErrors, TestCase class TestCorniceSetup(TestCase): - def setUp(self): self.config = testing.setUp() def _get_app(self): - self.config.include('cornice') + self.config.include("cornice") - failing_service = Service(name='failing', path='/fail') - failing_service.add_view('GET', lambda r: 1 / 0) + failing_service = Service(name="failing", path="/fail") + failing_service.add_view("GET", lambda r: 1 / 0) self.config.add_cornice_service(failing_service) return TestApp(CatchErrors(self.config.make_wsgi_app())) def test_exception_handling_is_included_by_default(self): app = self._get_app() - with mock.patch('cornice.pyramidhook.apply_filters', - wraps=apply_filters) as mocked: - app.post('/foo', status=404) + with mock.patch("cornice.pyramidhook.apply_filters", wraps=apply_filters) as mocked: + app.post("/foo", status=404) self.assertTrue(mocked.called) def test_exception_handling_can_be_disabled(self): self.config.add_settings(handle_exceptions=False) app = self._get_app() - with mock.patch('cornice.pyramidhook.apply_filters', - wraps=apply_filters) as mocked: - app.post('/foo', status=404) + with mock.patch("cornice.pyramidhook.apply_filters", wraps=apply_filters) as mocked: + app.post("/foo", status=404) self.assertFalse(mocked.called) - def test_exception_handling_is_included_by_default(self): - app = self._get_app() - with mock.patch('cornice.pyramidhook.apply_filters', - wraps=apply_filters) as mocked: - app.post('/foo', status=404) - self.assertTrue(mocked.called) - def test_exception_handling_raises_uncaught_errors(self): app = self._get_app() - self.assertRaises(ZeroDivisionError, app.get, '/fail') + self.assertRaises(ZeroDivisionError, app.get, "/fail") diff --git a/tests/test_pyramidhook.py b/tests/test_pyramidhook.py index cd1b3561..59fab1e2 100644 --- a/tests/test_pyramidhook.py +++ b/tests/test_pyramidhook.py @@ -10,9 +10,7 @@ from pyramid import testing from pyramid.exceptions import PredicateMismatch -from pyramid.httpexceptions import ( - HTTPOk, HTTPForbidden, HTTPNotFound, HTTPMethodNotAllowed -) +from pyramid.httpexceptions import HTTPOk, HTTPForbidden, HTTPNotFound, HTTPMethodNotAllowed from pyramid.csrf import CookieCSRFStoragePolicy from pyramid.response import Response from pyramid.security import Allow, Deny, NO_PERMISSION_REQUIRED @@ -30,11 +28,13 @@ def my_acl(request): - return [(Allow, 'alice', 'read'), - (Allow, 'bob', 'write'), - (Deny, 'carol', 'write'), - (Allow, 'dan', ('write', 'update')), - ] + return [ + (Allow, "alice", "read"), + (Allow, "bob", "write"), + (Deny, "carol", "write"), + (Allow, "dan", ("write", "update")), + ] + class MyFactory(object): def __init__(self, request): @@ -43,6 +43,7 @@ def __init__(self, request): def __acl__(self): return my_acl(self.request) + service = Service(name="service", path="/service", factory=MyFactory) @@ -50,15 +51,17 @@ def __acl__(self): def return_404(request): raise HTTPNotFound() -@service.put(permission='update') + +@service.put(permission="update") def update_view(request): return "updated_view" -@service.patch(permission='write') +@service.patch(permission="write") def return_yay(request): return "yay" + @service.delete() def delete_view(request): request.response.status = 204 @@ -71,28 +74,27 @@ def __init__(self, request, context=None): def get_fresh_air(self): resp = Response() - resp.text = u'air with ' + repr(self.context) + resp.text = "air with " + repr(self.context) return resp def make_it_fresh(self, response): - response.text = u'fresh ' + response.text + response.text = "fresh " + response.text return response def check_temperature(self, request, **kw): - if not 'X-Temperature' in request.headers: - request.errors.add('header', 'X-Temperature') + if not "X-Temperature" in request.headers: + request.errors.add("header", "X-Temperature") -tc = Service(name="TemperatureCooler", path="/fresh-air", - klass=TemperatureCooler, factory=dummy_factory) -tc.add_view("GET", "get_fresh_air", filters=('make_it_fresh',), - validators=('check_temperature',)) +tc = Service( + name="TemperatureCooler", path="/fresh-air", klass=TemperatureCooler, factory=dummy_factory +) +tc.add_view("GET", "get_fresh_air", filters=("make_it_fresh",), validators=("check_temperature",)) -class TestService(TestCase): +class TestService(TestCase): def setUp(self): - self.config = testing.setUp( - settings={'pyramid.debug_authorization': True}) + self.config = testing.setUp(settings={"pyramid.debug_authorization": True}) # Set up debug_authorization logging to console logging.basicConfig(level=logging.DEBUG) @@ -104,7 +106,7 @@ def setUp(self): self.authz_policy = ACLAuthorizationPolicy() self.config.set_authorization_policy(self.authz_policy) - self.authn_policy = AuthTktAuthenticationPolicy('$3kr1t') + self.authn_policy = AuthTktAuthenticationPolicy("$3kr1t") self.config.set_authentication_policy(self.authn_policy) self.config.scan("tests.test_service") @@ -129,52 +131,54 @@ def test_204(self): def test_acl_support_unauthenticated_service_patch(self): # calling a view with permissions without an auth'd user => 403 - self.app.patch('/service', status=HTTPForbidden.code) + self.app.patch("/service", status=HTTPForbidden.code) def test_acl_support_authenticated_allowed_service_patch(self): - with mock.patch.object(self.authn_policy, 'unauthenticated_userid', - return_value='bob'): - result = self.app.patch('/service', status=HTTPOk.code) + with mock.patch.object(self.authn_policy, "unauthenticated_userid", return_value="bob"): + result = self.app.patch("/service", status=HTTPOk.code) self.assertEqual("yay", result.json) # The other user with 'write' permission - with mock.patch.object(self.authn_policy, 'unauthenticated_userid', - return_value='dan'): - result = self.app.patch('/service', status=HTTPOk.code) + with mock.patch.object(self.authn_policy, "unauthenticated_userid", return_value="dan"): + result = self.app.patch("/service", status=HTTPOk.code) self.assertEqual("yay", result.json) def test_acl_support_authenticated_valid_user_wrong_permission_service_patch(self): - with mock.patch.object(self.authn_policy, 'unauthenticated_userid', return_value='alice'): - self.app.patch('/service', status=HTTPForbidden.code) + with mock.patch.object(self.authn_policy, "unauthenticated_userid", return_value="alice"): + self.app.patch("/service", status=HTTPForbidden.code) def test_acl_support_authenticated_valid_user_permission_denied_service_patch(self): - with mock.patch.object(self.authn_policy, 'unauthenticated_userid', return_value='carol'): - self.app.patch('/service', status=HTTPForbidden.code) + with mock.patch.object(self.authn_policy, "unauthenticated_userid", return_value="carol"): + self.app.patch("/service", status=HTTPForbidden.code) def test_acl_support_authenticated_invalid_user_service_patch(self): - with mock.patch.object(self.authn_policy, 'unauthenticated_userid', return_value='mallory'): - self.app.patch('/service', status=HTTPForbidden.code) + with mock.patch.object( + self.authn_policy, "unauthenticated_userid", return_value="mallory" + ): + self.app.patch("/service", status=HTTPForbidden.code) def test_acl_support_authenticated_allowed_service_put(self): - with mock.patch.object(self.authn_policy, 'unauthenticated_userid', return_value='dan'): - result = self.app.put('/service', status=HTTPOk.code) + with mock.patch.object(self.authn_policy, "unauthenticated_userid", return_value="dan"): + result = self.app.put("/service", status=HTTPOk.code) self.assertEqual("updated_view", result.json) def test_acl_support_authenticated_valid_user_wrong_permission_service_put(self): - with mock.patch.object(self.authn_policy, 'unauthenticated_userid', return_value='bob'): - self.app.put('/service', status=HTTPForbidden.code) + with mock.patch.object(self.authn_policy, "unauthenticated_userid", return_value="bob"): + self.app.put("/service", status=HTTPForbidden.code) def test_acl_support_authenticated_valid_user_permission_denied_service_put(self): - with mock.patch.object(self.authn_policy, 'unauthenticated_userid', return_value='carol'): - self.app.put('/service', status=HTTPForbidden.code) + with mock.patch.object(self.authn_policy, "unauthenticated_userid", return_value="carol"): + self.app.put("/service", status=HTTPForbidden.code) def test_acl_support_authenticated_invalid_user_service_put(self): - with mock.patch.object(self.authn_policy, 'unauthenticated_userid', return_value='mallory'): - self.app.put('/service', status=HTTPForbidden.code) + with mock.patch.object( + self.authn_policy, "unauthenticated_userid", return_value="mallory" + ): + self.app.put("/service", status=HTTPForbidden.code) def test_class_support(self): - self.app.get('/fresh-air', status=400) - resp = self.app.get('/fresh-air', headers={'X-Temperature': '50'}) - self.assertEqual(resp.body, b'fresh air with context!') + self.app.get("/fresh-air", status=400) + resp = self.app.get("/fresh-air", headers={"X-Temperature": "50"}) + self.assertEqual(resp.body, b"fresh air with context!") class WrapperService(Service): @@ -183,16 +187,18 @@ def upper_wrapper(func): def upperizer(*args, **kwargs): result = func(*args, **kwargs) return result.upper() + return upperizer + return upper_wrapper -wrapper_service = WrapperService(name='wrapperservice', path='/wrapperservice') +wrapper_service = WrapperService(name="wrapperservice", path="/wrapperservice") @wrapper_service.get() def return_foo(request): - return 'foo' + return "foo" class TestServiceWithWrapper(TestCase): @@ -206,8 +212,8 @@ def tearDown(self): testing.tearDown() def test_wrapped(self): - result = self.app.get('/wrapperservice') - self.assertEqual(result.json, 'FOO') + result = self.app.get("/wrapperservice") + self.assertEqual(result.json, "FOO") def test_func_name_undecorated_function(self): self.assertEqual("my_acl", func_name(my_acl)) @@ -219,7 +225,9 @@ def test_func_name_string(self): self.assertEqual("some_string", func_name("some_string")) def test_func_name_class_method(self): - self.assertEqual("TestServiceWithWrapper.test_wrapped", func_name(TestServiceWithWrapper.test_wrapped)) + self.assertEqual( + "TestServiceWithWrapper.test_wrapped", func_name(TestServiceWithWrapper.test_wrapped) + ) class TestNosniffHeader(TestCase): @@ -230,12 +238,12 @@ def setUp(self): self.app = TestApp(CatchErrors(self.config.make_wsgi_app())) def test_no_sniff_is_added_to_responses(self): - response = self.app.get('/wrapperservice') - self.assertEqual(response.headers['X-Content-Type-Options'], 'nosniff') + response = self.app.get("/wrapperservice") + self.assertEqual(response.headers["X-Content-Type-Options"], "nosniff") test_service = Service(name="jardinet", path="/jardinet") -test_service.add_view('GET', lambda request: request.current_service.name) +test_service.add_view("GET", lambda request: request.current_service.name) class TestCurrentService(TestCase): @@ -251,81 +259,80 @@ def test_current_service_on_request(self): class TestRouteWithTraverse(TestCase): - def test_route_construction(self): config = mock.MagicMock() config.add_route = mock.MagicMock() register_service_views(config, test_service) - config.add_route.assert_called_with('jardinet', '/jardinet') + config.add_route.assert_called_with("jardinet", "/jardinet") def test_route_with_prefix(self): config = testing.setUp(settings={}) config.add_route = mock.MagicMock() - config.route_prefix = '/prefix' + config.route_prefix = "/prefix" config.registry.cornice_services = {} - config.add_directive('add_cornice_service', register_service_views) + config.add_directive("add_cornice_service", register_service_views) config.scan("tests.test_pyramidhook") services = config.registry.cornice_services - self.assertTrue('/prefix/wrapperservice' in services) + self.assertTrue("/prefix/wrapperservice" in services) class TestRouteFromPyramid(TestCase): - def setUp(self): self.config = testing.setUp() self.config.include("cornice") - self.config.add_route('proute', '/from_pyramid') + self.config.add_route("proute", "/from_pyramid") self.config.scan("tests.test_pyramidhook") def handle_response(request): - return {'service': request.current_service.name, - 'route': request.matched_route.name} + return {"service": request.current_service.name, "route": request.matched_route.name} + rserv = Service(name="ServiceWPyramidRoute", pyramid_route="proute") - rserv.add_view('GET', handle_response) + rserv.add_view("GET", handle_response) register_service_views(self.config, rserv) self.app = TestApp(CatchErrors(self.config.make_wsgi_app())) def test_service_routing(self): - result = self.app.get('/from_pyramid', status=200) - self.assertEqual('proute', result.json['route']) - self.assertEqual('ServiceWPyramidRoute', result.json['service']) - + result = self.app.get("/from_pyramid", status=200) + self.assertEqual("proute", result.json["route"]) + self.assertEqual("ServiceWPyramidRoute", result.json["service"]) def test_no_route_or_path(self): with self.assertRaises(TypeError): - Service(name="broken service",) + Service( + name="broken service", + ) class TestPrefixRouteFromPyramid(TestCase): - def setUp(self): self.config = testing.setUp() - self.config.route_prefix = '/prefix' + self.config.route_prefix = "/prefix" self.config.include("cornice") - self.config.add_route('proute', '/from_pyramid') + self.config.add_route("proute", "/from_pyramid") self.config.scan("tests.test_pyramidhook") def handle_response(request): - return {'service': request.current_service.name, - 'route': request.matched_route.name} + return {"service": request.current_service.name, "route": request.matched_route.name} + rserv = Service(name="ServiceWPyramidRoute", pyramid_route="proute") - rserv.add_view('GET', handle_response) + rserv.add_view("GET", handle_response) register_service_views(self.config, rserv) self.app = TestApp(CatchErrors(self.config.make_wsgi_app())) def test_service_routing(self): - result = self.app.get('/prefix/from_pyramid', status=200) - self.assertEqual('proute', result.json['route']) - self.assertEqual('ServiceWPyramidRoute', result.json['service']) - + result = self.app.get("/prefix/from_pyramid", status=200) + self.assertEqual("proute", result.json["route"]) + self.assertEqual("ServiceWPyramidRoute", result.json["service"]) def test_no_route_or_path(self): with self.assertRaises(TypeError): - Service(name="broken service",) + Service( + name="broken service", + ) def test_current_service(self): pyramid_app = self.app.app.app @@ -334,6 +341,7 @@ def test_current_service(self): request.registry = pyramid_app.registry assert current_service(request) + class TestServiceWithNonpickleableSchema(TestCase): def setUp(self): self.config = testing.setUp() @@ -344,17 +352,16 @@ def tearDown(self): def test(self): # Compiled regexs are, apparently, non-pickleable - service = Service(name="test", path="/", schema={'a': re.compile('')}) - service.add_view('GET', lambda _:_) + service = Service(name="test", path="/", schema={"a": re.compile("")}) + service.add_view("GET", lambda _: _) register_service_views(self.config, service) - class TestFallbackRegistration(TestCase): def setUp(self): self.config = testing.setUp() - self.config.add_view_predicate('content_type', ContentTypePredicate) - self.config.set_csrf_storage_policy(CookieCSRFStoragePolicy(domain='localhost')) + self.config.add_view_predicate("content_type", ContentTypePredicate) + self.config.set_csrf_storage_policy(CookieCSRFStoragePolicy(domain="localhost")) self.config.set_default_csrf_options(require_csrf=True) self.config.registry.cornice_services = {} @@ -366,37 +373,35 @@ def test_fallback_permission(self): Fallback view should be registered with NO_PERMISSION_REQUIRED Fixes: https://github.com/mozilla-services/cornice/issues/245 """ - service = Service(name='fallback-test', path='/') - service.add_view('GET', lambda _:_) + service = Service(name="fallback-test", path="/") + service.add_view("GET", lambda _: _) register_service_views(self.config, service) # This is a bit baroque introspector = self.config.introspector - views = introspector.get_category('views') - fallback_views = [i for i in views - if i['introspectable']['route_name']=='fallback-test'] + views = introspector.get_category("views") + fallback_views = [i for i in views if i["introspectable"]["route_name"] == "fallback-test"] for v in fallback_views: - if v['introspectable'].title == u'function cornice.pyramidhook._fallback_view': - permissions = [p['value'] for p in v['related'] if p.type_name == 'permission'] + if v["introspectable"].title == "function cornice.pyramidhook._fallback_view": + permissions = [p["value"] for p in v["related"] if p.type_name == "permission"] self.assertIn(NO_PERMISSION_REQUIRED, permissions) def test_fallback_no_predicate(self): - service = Service(name='fallback-test', path='/', - effective_principals=('group:admins',)) - service.add_view('GET', lambda _:_) + service = Service(name="fallback-test", path="/", effective_principals=("group:admins",)) + service.add_view("GET", lambda _: _) register_service_views(self.config, service) - self.config.include('cornice') + self.config.include("cornice") app = self.config.make_wsgi_app() testapp = TestApp(app) - testapp.get('/', status=404) - #self.assertRaises(PredicateMismatch, testapp.get, '/') + testapp.get("/", status=404) + # self.assertRaises(PredicateMismatch, testapp.get, '/') def test_fallback_no_required_csrf(self): - service = Service(name='fallback-csrf', path='/', content_type='application/json') - service.add_view('POST', lambda _:'', require_csrf=False) + service = Service(name="fallback-csrf", path="/", content_type="application/json") + service.add_view("POST", lambda _: "", require_csrf=False) register_service_views(self.config, service) - self.config.include('cornice') + self.config.include("cornice") app = self.config.make_wsgi_app() testapp = TestApp(app) - testapp.post('/', status=415, headers={'Content-Type': 'application/xml'}) + testapp.post("/", status=415, headers={"Content-Type": "application/xml"}) diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 396a46cd..c2b3118a 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -1,11 +1,11 @@ from unittest import mock +from cornice import CorniceRenderer +from cornice.renderer import JSONError, bytes_adapter from pyramid.interfaces import IJSONAdapter from pyramid.renderers import JSON from zope.interface import providedBy -from cornice import CorniceRenderer -from cornice.renderer import bytes_adapter, JSONError from .support import TestCase @@ -28,10 +28,7 @@ def test_renderer_is_pyramid_renderer_subclass(self): def test_renderer_has_bytes_adapter_by_default(self): renderer = CorniceRenderer() self.assertEqual( - renderer.components.adapters.lookup( - (providedBy(bytes()),), - IJSONAdapter - ), + renderer.components.adapters.lookup((providedBy(bytes()),), IJSONAdapter), bytes_adapter, ) @@ -54,7 +51,4 @@ def __json__(self, request): result = renderer.render_errors(request) self.assertIsInstance(result, JSONError) self.assertEqual(result.status_int, 418) - self.assertEqual( - result.json_body, - {"status": "error", "errors": ["error_1", "error_2"]} - ) + self.assertEqual(result.json_body, {"status": "error", "errors": ["error_1", "error_2"]}) diff --git a/tests/test_resource.py b/tests/test_resource.py index ceb9ee70..92db0561 100644 --- a/tests/test_resource.py +++ b/tests/test_resource.py @@ -1,34 +1,30 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. -import json import functools +import json from unittest import mock +from cornice.resource import resource, view from pyramid import testing from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy -from pyramid.security import Allow -from pyramid.httpexceptions import ( - HTTPOk, HTTPForbidden -) from pyramid.exceptions import ConfigurationError +from pyramid.httpexceptions import HTTPForbidden, HTTPOk +from pyramid.security import Allow from webtest import TestApp -from unittest import skip -from cornice.resource import resource, view +from .support import CatchErrors, TestCase, dummy_factory -from .support import TestCase, CatchErrors, dummy_factory +USERS = {1: {"name": "gawel"}, 2: {"name": "tarek"}} -USERS = {1: {'name': 'gawel'}, 2: {'name': 'tarek'}} def my_collection_acl(request): - return [(Allow, 'alice', 'read')] + return [(Allow, "alice", "read")] -@resource(collection_path='/thing', path='/thing/{id}', - name='thing_service') +@resource(collection_path="/thing", path="/thing/{id}", name="thing_service") class Thing(object): """This is a thing.""" @@ -39,13 +35,12 @@ def __init__(self, request, context=None): def __acl__(self): return my_collection_acl(self.request) - @view(permission='read') + @view(permission="read") def collection_get(self): - return 'yay' + return "yay" -@resource(collection_path='/users', path='/users/{id}', - name='user_service', factory=dummy_factory) +@resource(collection_path="/users", path="/users/{id}", name="user_service", factory=dummy_factory) class User(object): """My user resource.""" @@ -54,32 +49,33 @@ def __init__(self, request, context=None): self.context = context def collection_get(self): - return {'users': list(USERS.keys())} + return {"users": list(USERS.keys())} - @view(renderer='jsonp', accept='application/javascript') - @view(renderer='json') + @view(renderer="jsonp", accept="application/javascript") + @view(renderer="json") def get(self): - return USERS.get(int(self.request.matchdict['id'])) + return USERS.get(int(self.request.matchdict["id"])) - @view(renderer='json', accept='application/json') + @view(renderer="json", accept="application/json") def collection_post(self): - return {'test': 'yeah'} + return {"test": "yeah"} def patch(self): - return {'test': 'yeah'} + return {"test": "yeah"} def collection_patch(self): - return {'test': 'yeah'} + return {"test": "yeah"} def put(self): return dict(type=repr(self.context)) class TestResourceWarning(TestCase): - @mock.patch('warnings.warn') + @mock.patch("warnings.warn") def test_path_clash(self, mocked_warn): - @resource(collection_path='/badthing/{id}', path='/badthing/{id}', - name='bad_thing_service') + @resource( + collection_path="/badthing/{id}", path="/badthing/{id}", name="bad_thing_service" + ) class BadThing(object): def __init__(self, request, context=None): pass @@ -87,48 +83,52 @@ def __init__(self, request, context=None): msg = "Warning: collection_path and path are not distinct." mocked_warn.assert_called_with(msg) - @mock.patch('warnings.warn') + @mock.patch("warnings.warn") def test_routes_clash(self, mocked_warn): - @resource(collection_pyramid_route='some_route', - pyramid_route='some_route', name='bad_thing_service') + @resource( + collection_pyramid_route="some_route", + pyramid_route="some_route", + name="bad_thing_service", + ) class BadThing(object): def __init__(self, request, context=None): pass - msg = "Warning: collection_pyramid_route and " \ - "pyramid_route are not distinct." + msg = "Warning: collection_pyramid_route and " "pyramid_route are not distinct." mocked_warn.assert_called_with(msg) - def test_routes_with_paths(self): with self.assertRaises(ValueError): - @resource(collection_path='/some_route', - pyramid_route='some_route', name='bad_thing_service') + + @resource( + collection_path="/some_route", pyramid_route="some_route", name="bad_thing_service" + ) class BadThing(object): def __init__(self, request, context=None): pass def test_routes_with_paths_reversed(self): with self.assertRaises(ValueError): - @resource(collection_pyramid_route='some_route', - path='/some_route', name='bad_thing_service') + + @resource( + collection_pyramid_route="some_route", path="/some_route", name="bad_thing_service" + ) class BadThing(object): def __init__(self, request, context=None): pass class TestResource(TestCase): - def setUp(self): from pyramid.renderers import JSONP self.config = testing.setUp() - self.config.add_renderer('jsonp', JSONP(param_name='callback')) + self.config.add_renderer("jsonp", JSONP(param_name="callback")) self.config.include("cornice") self.authz_policy = ACLAuthorizationPolicy() self.config.set_authorization_policy(self.authz_policy) - self.authn_policy = AuthTktAuthenticationPolicy('$3kr1t') + self.authn_policy = AuthTktAuthenticationPolicy("$3kr1t") self.config.set_authentication_policy(self.authn_policy) self.config.scan("tests.test_resource") self.app = TestApp(CatchErrors(self.config.make_wsgi_app())) @@ -137,106 +137,115 @@ def tearDown(self): testing.tearDown() def test_basic_resource(self): - self.assertEqual(self.app.get("/users").json, {'users': [1, 2]}) + self.assertEqual(self.app.get("/users").json, {"users": [1, 2]}) - self.assertEqual(self.app.get("/users/1").json, {'name': 'gawel'}) + self.assertEqual(self.app.get("/users/1").json, {"name": "gawel"}) - resp = self.app.get("/users/1?callback=test", - headers={'Accept': 'application/javascript'}) + resp = self.app.get("/users/1?callback=test", headers={"Accept": "application/javascript"}) self.assertIn(b'test({"name": "gawel"})', resp.body, msg=resp.body) - @mock.patch('cornice.resource.Service') + @mock.patch("cornice.resource.Service") def test_without_collection_path_has_one_service(self, mocked_service): - @resource(path='/nocollection/{id}', name='nocollection') + @resource(path="/nocollection/{id}", name="nocollection") class NoCollection(object): def __init__(self, request, context=None): pass + self.assertEqual(mocked_service.call_count, 1) def test_accept_headers(self): # the accept headers should work even in case they're specified in a # resource method self.assertEqual( - self.app.post("/users", headers={'Accept': 'application/json'}, - params=json.dumps({'test': 'yeah'})).json, - {'test': 'yeah'}) + self.app.post( + "/users", + headers={"Accept": "application/json"}, + params=json.dumps({"test": "yeah"}), + ).json, + {"test": "yeah"}, + ) def patch(self, *args, **kwargs): - return self.app._gen_request('PATCH', *args, **kwargs) + return self.app._gen_request("PATCH", *args, **kwargs) def test_head_and_patch(self): self.app.head("/users") self.app.head("/users/1") - self.assertEqual( - self.patch("/users").json, - {'test': 'yeah'}) + self.assertEqual(self.patch("/users").json, {"test": "yeah"}) - self.assertEqual( - self.patch("/users/1").json, - {'test': 'yeah'}) + self.assertEqual(self.patch("/users/1").json, {"test": "yeah"}) def test_context_factory(self): - self.assertEqual(self.app.put('/users/1').json, {'type': 'context!'}) + self.assertEqual(self.app.put("/users/1").json, {"type": "context!"}) def test_explicit_collection_service_name(self): route_url = testing.DummyRequest().route_url # service must exist - self.assertTrue(route_url('collection_user_service')) + self.assertTrue(route_url("collection_user_service")) def test_explicit_service_name(self): route_url = testing.DummyRequest().route_url - self.assertTrue(route_url('user_service', id=42)) # service must exist + self.assertTrue(route_url("user_service", id=42)) # service must exist - @mock.patch('cornice.resource.Service') + @mock.patch("cornice.resource.Service") def test_factory_is_autowired(self, mocked_service): - @resource(collection_path='/list', path='/list/{id}', name='list') + @resource(collection_path="/list", path="/list/{id}", name="list") class List(object): pass - factory_args = [kw.get('factory') for _, kw in mocked_service.call_args_list] + + factory_args = [kw.get("factory") for _, kw in mocked_service.call_args_list] self.assertEqual([List, List], factory_args) def test_acl_is_deprecated(self): def custom_acl(request): return [] + with self.assertRaises(ConfigurationError): - @resource(collection_path='/list', path='/list/{id}', name='list', - collection_acl=custom_acl, - acl=custom_acl) + + @resource( + collection_path="/list", + path="/list/{id}", + name="list", + collection_acl=custom_acl, + acl=custom_acl, + ) class List(object): pass def test_acl_support_unauthenticated_thing_get(self): # calling a view with permissions without an auth'd user => 403 - self.app.get('/thing', status=HTTPForbidden.code) + self.app.get("/thing", status=HTTPForbidden.code) def test_acl_support_unauthenticated_forbidden_thing_get(self): # calling a view with permissions without an auth'd user => 403 - with mock.patch.object(self.authn_policy, 'authenticated_userid', return_value=None): - result = self.app.get('/thing', status=HTTPForbidden.code) + with mock.patch.object(self.authn_policy, "authenticated_userid", return_value=None): + self.app.get("/thing", status=HTTPForbidden.code) def test_acl_support_authenticated_allowed_thing_get(self): - with mock.patch.object(self.authn_policy, 'unauthenticated_userid', return_value='alice'): - with mock.patch.object(self.authn_policy, 'authenticated_userid', return_value='alice'): - result = self.app.get('/thing', status=HTTPOk.code) + with mock.patch.object(self.authn_policy, "unauthenticated_userid", return_value="alice"): + with mock.patch.object( + self.authn_policy, "authenticated_userid", return_value="alice" + ): + result = self.app.get("/thing", status=HTTPOk.code) self.assertEqual("yay", result.json) def test_service_wrapped_resource(self): resources = { - 'thing_service': Thing, - 'user_service': User, + "thing_service": Thing, + "user_service": User, } for name, service in ( - (x.name, x) for x in self.config.registry.cornice_services.values() + (x.name, x) + for x in self.config.registry.cornice_services.values() if x.name in resources ): for attr in functools.WRAPPER_ASSIGNMENTS: with self.subTest(service=name, attribute=attr): self.assertEqual( - getattr(resources[name], attr, None), - getattr(service, attr, None) + getattr(resources[name], attr, None), getattr(service, attr, None) ) @@ -248,8 +257,9 @@ class NonAutocommittingConfigurationTestResource(TestCase): def setUp(self): from pyramid.renderers import JSONP + self.config = testing.setUp(autocommit=False) - self.config.add_renderer('jsonp', JSONP(param_name='callback')) + self.config.add_renderer("jsonp", JSONP(param_name="callback")) self.config.include("cornice") self.config.scan("tests.test_resource") self.app = TestApp(CatchErrors(self.config.make_wsgi_app())) @@ -258,4 +268,4 @@ def tearDown(self): testing.tearDown() def test_get(self): - self.app.get('/users/1') + self.app.get("/users/1") diff --git a/tests/test_resource_callable.py b/tests/test_resource_callable.py index 0e7c9a2a..717f51d2 100644 --- a/tests/test_resource_callable.py +++ b/tests/test_resource_callable.py @@ -3,51 +3,48 @@ import json +from cornice.resource import resource, view from pyramid import testing from webtest import TestApp -from cornice.resource import resource, view +from .support import CatchErrors, TestCase -from .support import TestCase, CatchErrors -FRUITS = {1: {'name': 'apple'}, 2: {'name': 'orange'}} +FRUITS = {1: {"name": "apple"}, 2: {"name": "orange"}} def _accept(request): - return ('text/plain', 'application/json') + return ("text/plain", "application/json") def _content_type(request): - return ('text/plain', 'application/json') + return ("text/plain", "application/json") -@resource(collection_path='/fruits', path='/fruits/{id}', - name='fruit_service', accept=_accept) +@resource(collection_path="/fruits", path="/fruits/{id}", name="fruit_service", accept=_accept) class Fruit(object): - def __init__(self, request, context=None): self.request = request self.context = context def collection_get(self): - return {'fruits': list(FRUITS.keys())} + return {"fruits": list(FRUITS.keys())} - @view(renderer='json', accept=_accept) + @view(renderer="json", accept=_accept) def get(self): - return FRUITS.get(int(self.request.matchdict['id'])) + return FRUITS.get(int(self.request.matchdict["id"])) - @view(renderer='json', accept=_accept, content_type=_content_type) + @view(renderer="json", accept=_accept, content_type=_content_type) def collection_post(self): - return {'test': 'yeah'} + return {"test": "yeah"} class TestResource(TestCase): - def setUp(self): from pyramid.renderers import JSONP self.config = testing.setUp() - self.config.add_renderer('jsonp', JSONP(param_name='callback')) + self.config.add_renderer("jsonp", JSONP(param_name="callback")) self.config.include("cornice") self.config.scan("tests.test_resource_callable") @@ -58,47 +55,55 @@ def tearDown(self): def test_accept_headers_get(self): self.assertEqual( - self.app.get("/fruits", headers={'Accept': 'text/plain'}).body, - b'{"fruits": [1, 2]}') + self.app.get("/fruits", headers={"Accept": "text/plain"}).body, b'{"fruits": [1, 2]}' + ) self.assertEqual( - self.app.get("/fruits", headers={'Accept': 'application/json'}).json, - {'fruits': [1, 2]}) + self.app.get("/fruits", headers={"Accept": "application/json"}).json, + {"fruits": [1, 2]}, + ) self.assertEqual( - self.app.get("/fruits/1", headers={'Accept': 'text/plain'}).json, - {'name': 'apple'}) + self.app.get("/fruits/1", headers={"Accept": "text/plain"}).json, {"name": "apple"} + ) self.assertEqual( - self.app.get("/fruits/1", headers={'Accept': 'application/json'}).json, - {'name': 'apple'}) - + self.app.get("/fruits/1", headers={"Accept": "application/json"}).json, + {"name": "apple"}, + ) def test_accept_headers_post(self): self.assertEqual( - self.app.post("/fruits", headers={'Accept': 'text/plain', 'Content-Type': 'application/json'}, - params=json.dumps({'test': 'yeah'})).json, - {'test': 'yeah'}) + self.app.post( + "/fruits", + headers={"Accept": "text/plain", "Content-Type": "application/json"}, + params=json.dumps({"test": "yeah"}), + ).json, + {"test": "yeah"}, + ) self.assertEqual( - self.app.post("/fruits", headers={'Accept': 'application/json', 'Content-Type': 'application/json'}, - params=json.dumps({'test': 'yeah'})).json, - {'test': 'yeah'}) + self.app.post( + "/fruits", + headers={"Accept": "application/json", "Content-Type": "application/json"}, + params=json.dumps({"test": "yeah"}), + ).json, + {"test": "yeah"}, + ) def test_406(self): - self.app.get( - "/fruits", - headers={'Accept': 'text/xml'}, - status=406) + self.app.get("/fruits", headers={"Accept": "text/xml"}, status=406) - self.app.post( - "/fruits", - headers={'Accept': 'text/html'}, - params=json.dumps({'test': 'yeah'}), - status=406) + self.app.post( + "/fruits", + headers={"Accept": "text/html"}, + params=json.dumps({"test": "yeah"}), + status=406, + ) def test_415(self): self.app.post( "/fruits", - headers={'Accept': 'application/json', 'Content-Type': 'text/html'}, - status=415) + headers={"Accept": "application/json", "Content-Type": "text/html"}, + status=415, + ) diff --git a/tests/test_resource_custom_predicates.py b/tests/test_resource_custom_predicates.py index d6f50357..302716b5 100644 --- a/tests/test_resource_custom_predicates.py +++ b/tests/test_resource_custom_predicates.py @@ -1,14 +1,13 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. +from cornice.resource import resource, view from pyramid import testing from pyramid.authentication import AuthTktAuthenticationPolicy from pyramid.authorization import ACLAuthorizationPolicy from webtest import TestApp -from cornice.resource import resource -from cornice.resource import view -from .support import TestCase, CatchErrors +from .support import CatchErrors, TestCase class employeeType(object): @@ -16,88 +15,94 @@ def __init__(self, val, config): self.val = val def text(self): - return 'position = %s' % (self.val,) + return "position = %s" % (self.val,) phash = text def __call__(self, context, request): - if request.params.get('position') is not None: - position = request.params.get('position') + if request.params.get("position") is not None: + position = request.params.get("position") return position == self.val return False -@resource(collection_path='/company/employees', path='/company/employees/{id}', - name='Topmanagers', position=u'topmanager') +@resource( + collection_path="/company/employees", + path="/company/employees/{id}", + name="Topmanagers", + position="topmanager", +) class EManager(object): - def __init__(self, request, context=None): self.request = request self.context = context - @view(renderer='json', accept='application/json') + @view(renderer="json", accept="application/json") def collection_get(self): - return ['Topmanagers list get'] + return ["Topmanagers list get"] - @view(renderer='json', accept='application/json') + @view(renderer="json", accept="application/json") def get(self): - return {'get': 'Topmanagers'} + return {"get": "Topmanagers"} - @view(renderer='json', accept='application/json') + @view(renderer="json", accept="application/json") def collection_post(self): - return ['Topmanagers list post'] + return ["Topmanagers list post"] - @view(renderer='json', accept='application/json') + @view(renderer="json", accept="application/json") def patch(self): - return {'patch': 'Topmanagers'} + return {"patch": "Topmanagers"} - @view(renderer='json', accept='application/json') + @view(renderer="json", accept="application/json") def put(self): - return {'put': 'Topmanagers'} + return {"put": "Topmanagers"} -@resource(collection_path='/company/employees', path='/company/employees/{id}', - name='Supervisors', position=u'supervisor') +@resource( + collection_path="/company/employees", + path="/company/employees/{id}", + name="Supervisors", + position="supervisor", +) class ESupervisor(object): - def __init__(self, request, context=None): self.request = request self.context = context - @view(renderer='json', accept='application/json') + @view(renderer="json", accept="application/json") def collection_get(self): - return ['Supervisors list get'] + return ["Supervisors list get"] - @view(renderer='json', accept='application/json') + @view(renderer="json", accept="application/json") def get(self): - return {'get': 'Supervisors'} + return {"get": "Supervisors"} - @view(renderer='json', accept='application/json') + @view(renderer="json", accept="application/json") def collection_post(self): - return ['Supervisors list post'] + return ["Supervisors list post"] - @view(renderer='json', accept='application/json') + @view(renderer="json", accept="application/json") def patch(self): - return {'patch': 'Supervisors'} + return {"patch": "Supervisors"} - @view(renderer='json', accept='application/json') + @view(renderer="json", accept="application/json") def put(self): - return {'put': 'Supervisors'} + return {"put": "Supervisors"} class TestCustomPredicates(TestCase): - def setUp(self): from pyramid.renderers import JSONP + self.config = testing.setUp() - self.config.add_renderer('jsonp', JSONP(param_name='callback')) + self.config.add_renderer("jsonp", JSONP(param_name="callback")) self.config.include("cornice") self.authz_policy = ACLAuthorizationPolicy() self.config.set_authorization_policy(self.authz_policy) - self.authn_policy = AuthTktAuthenticationPolicy('$3kr1t') + self.authn_policy = AuthTktAuthenticationPolicy("$3kr1t") self.config.set_authentication_policy(self.authn_policy) - self.config.add_route_predicate('position', employeeType) + self.config.add_route_predicate("position", employeeType) self.config.scan("tests.test_resource_custom_predicates") self.app = TestApp(CatchErrors(self.config.make_wsgi_app())) @@ -106,64 +111,44 @@ def tearDown(self): def test_get_resource_predicates(self): # Tests for resource with name 'Supervisors' - res = self.app.get('/company/employees?position=supervisor').json - self.assertEqual(res[0], 'Supervisors list get') - res = self.app.get('/company/employees/2?position=supervisor').json - self.assertEqual(res['get'], 'Supervisors') + res = self.app.get("/company/employees?position=supervisor").json + self.assertEqual(res[0], "Supervisors list get") + res = self.app.get("/company/employees/2?position=supervisor").json + self.assertEqual(res["get"], "Supervisors") # Tests for resource with name 'Topmanagers' - res = self.app.get('/company/employees?position=topmanager').json - self.assertEqual(res[0], 'Topmanagers list get') - res = self.app.get('/company/employees/1?position=topmanager').json - self.assertEqual(res['get'], 'Topmanagers') + res = self.app.get("/company/employees?position=topmanager").json + self.assertEqual(res[0], "Topmanagers list get") + res = self.app.get("/company/employees/1?position=topmanager").json + self.assertEqual(res["get"], "Topmanagers") def test_post_resource_predicates(self): # Tests for resource with name 'Supervisors' - supervisor_data = { - 'name': 'Jimmy Arrow', - 'position': 'supervisor', - 'salary': 50000 - } - res = self.app.post('/company/employees', supervisor_data).json - self.assertEqual(res[0], 'Supervisors list post') + supervisor_data = {"name": "Jimmy Arrow", "position": "supervisor", "salary": 50000} + res = self.app.post("/company/employees", supervisor_data).json + self.assertEqual(res[0], "Supervisors list post") # Tests for resource with name 'Topmanagers' - topmanager_data = { - 'name': 'Jimmy Arrow', - 'position': 'topmanager', - 'salary': 30000 - } - res = self.app.post('/company/employees', topmanager_data).json - self.assertEqual(res[0], 'Topmanagers list post') + topmanager_data = {"name": "Jimmy Arrow", "position": "topmanager", "salary": 30000} + res = self.app.post("/company/employees", topmanager_data).json + self.assertEqual(res[0], "Topmanagers list post") def test_patch_resource_predicates(self): # Tests for resource with name 'Supervisors' - res = self.app.patch( - '/company/employees/2?position=supervisor', - {'salary': 1001} - ).json - self.assertEqual(res['patch'], 'Supervisors') + res = self.app.patch("/company/employees/2?position=supervisor", {"salary": 1001}).json + self.assertEqual(res["patch"], "Supervisors") # Tests for resource with name 'Topmanagers' - res = self.app.patch( - '/company/employees/1?position=topmanager', - {'salary': 2002} - ).json - self.assertEqual(res['patch'], 'Topmanagers') + res = self.app.patch("/company/employees/1?position=topmanager", {"salary": 2002}).json + self.assertEqual(res["patch"], "Topmanagers") def test_put_resource_predicates(self): # Tests for resource with name 'Supervisors' - supervisor_data = { - 'position': 'supervisor', - 'salary': 53000 - } - res = self.app.put('/company/employees/2', supervisor_data).json - self.assertEqual(res['put'], 'Supervisors') + supervisor_data = {"position": "supervisor", "salary": 53000} + res = self.app.put("/company/employees/2", supervisor_data).json + self.assertEqual(res["put"], "Supervisors") # Tests for resource with name 'Topmanagers' - topmanager_data = { - 'position': 'topmanager', - 'salary': 33000 - } - res = self.app.put('/company/employees/1', topmanager_data).json - self.assertEqual(res['put'], 'Topmanagers') + topmanager_data = {"position": "topmanager", "salary": 33000} + res = self.app.put("/company/employees/1", topmanager_data).json + self.assertEqual(res["put"], "Topmanagers") diff --git a/tests/test_resource_traverse.py b/tests/test_resource_traverse.py index f9ce2218..7ffd2ae6 100644 --- a/tests/test_resource_traverse.py +++ b/tests/test_resource_traverse.py @@ -1,19 +1,17 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. +from cornice.resource import resource, view from pyramid import testing from webtest import TestApp -from cornice.resource import resource, view - -from .support import TestCase, CatchErrors +from .support import CatchErrors, TestCase -FRUITS = {'1': {'name': 'apple'}, '2': {'name': 'orange'}} +FRUITS = {"1": {"name": "apple"}, "2": {"name": "orange"}} class FruitFactory(object): - def __init__(self, request): self.request = request @@ -22,49 +20,47 @@ def __getitem__(self, key): @resource( - collection_path='/fruits/', + collection_path="/fruits/", collection_factory=FruitFactory, - collection_traverse='', - path='/fruits/{fruit_id}/', + collection_traverse="", + path="/fruits/{fruit_id}/", factory=FruitFactory, - name='fruit_service', - traverse='/{fruit_id}' + name="fruit_service", + traverse="/{fruit_id}", ) class Fruit(object): - def __init__(self, request, context): self.request = request self.context = context def collection_get(self): - return {'fruits': list(FRUITS.keys())} + return {"fruits": list(FRUITS.keys())} - @view(renderer='json') + @view(renderer="json") def get(self): return self.context class TestResourceTraverse(TestCase): - def setUp(self): self.config = testing.setUp() - self.config.include('cornice') + self.config.include("cornice") - self.config.scan('tests.test_resource_traverse') + self.config.scan("tests.test_resource_traverse") self.app = TestApp(CatchErrors(self.config.make_wsgi_app())) def tearDown(self): testing.tearDown() def test_collection_traverse(self): - resp = self.app.get('/fruits/').json - self.assertEqual(sorted(resp['fruits']), ['1', '2']) + resp = self.app.get("/fruits/").json + self.assertEqual(sorted(resp["fruits"]), ["1", "2"]) def test_traverse(self): - resp = self.app.get('/fruits/1/') - self.assertEqual(resp.json, {'name': 'apple'}) + resp = self.app.get("/fruits/1/") + self.assertEqual(resp.json, {"name": "apple"}) - resp = self.app.get('/fruits/2/') - self.assertEqual(resp.json, {'name': 'orange'}) + resp = self.app.get("/fruits/2/") + self.assertEqual(resp.json, {"name": "orange"}) - self.app.get('/fruits/3/', status=404) + self.app.get("/fruits/3/", status=404) diff --git a/tests/test_service.py b/tests/test_service.py index a42081e6..82b6bcc9 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -3,15 +3,13 @@ # You can obtain one at http://mozilla.org/MPL/2.0/. from unittest import mock -from pyramid.exceptions import ConfigurationError -from pyramid.interfaces import IRendererFactory - from cornice.resource import resource -from cornice.service import (Service, get_services, clear_services, - decorate_view, _UnboundView) +from cornice.service import Service, _UnboundView, clear_services, decorate_view, get_services from cornice.util import func_name +from pyramid.exceptions import ConfigurationError +from pyramid.interfaces import IRendererFactory -from .support import TestCase, DummyRequest +from .support import DummyRequest, TestCase def _validator(req): @@ -26,7 +24,7 @@ def _stub(req): return None -@resource(collection_path='/pets', path='/pets/{id}') +@resource(collection_path="/pets", path="/pets/{id}") class DummyAPI(object): last_request = None last_context = None @@ -36,11 +34,10 @@ def __init__(self, request, context=None): DummyAPI.last_context = context def collection_get(self): - return ['douggy', 'rusty'] + return ["douggy", "rusty"] class TestService(TestCase): - def tearDown(self): clear_services() @@ -54,17 +51,19 @@ def test_service_instantiation(self): self.assertEqual(service.renderer, "html") # test that lists are also set - validators = [lambda x: True, ] + validators = [ + lambda x: True, + ] service = Service("coconuts", "/migrate", validators=validators) self.assertEqual(service.validators, validators) def test_representation(self): service = Service("coconuts", "/migrate") - self.assertEqual(repr(service), '') + self.assertEqual(repr(service), "") def test_representation_path(self): service = Service("coconuts", pyramid_route="some_route") - self.assertEqual(repr(service), '') + self.assertEqual(repr(service), "") def test_get_arguments(self): service = Service("coconuts", "/migrate") @@ -77,35 +76,34 @@ def test_get_arguments(self): # passed at instantiation time as default values service = Service("coconuts", "/migrate", renderer="html") args = service.get_arguments({}) - self.assertEqual(args['renderer'], 'html') + self.assertEqual(args["renderer"], "html") # if we specify another renderer for this service, despite the fact # that one is already set in the instance, this one should be used - args = service.get_arguments({'renderer': 'foobar'}) - self.assertEqual(args['renderer'], 'foobar') + args = service.get_arguments({"renderer": "foobar"}) + self.assertEqual(args["renderer"], "foobar") # test that list elements are not overwritten # define a validator for the needs of the test service = Service("vaches", "/fetchez", validators=(_validator,)) self.assertEqual(len(service.validators), 1) - args = service.get_arguments({'validators': (_validator2,)}) + args = service.get_arguments({"validators": (_validator2,)}) # the list of validators didn't changed self.assertEqual(len(service.validators), 1) # but the one returned contains 2 validators - self.assertEqual(len(args['validators']), 2) + self.assertEqual(len(args["validators"]), 2) # test that exclude effectively removes the items from the list of # validators / filters it returns, without removing it from the ones # registered for the service. - service = Service("open bar", "/bar", validators=(_validator, - _validator2)) + service = Service("open bar", "/bar", validators=(_validator, _validator2)) self.assertEqual(service.validators, [_validator, _validator2]) args = service.get_arguments({"exclude": _validator2}) - self.assertEqual(args['validators'], [_validator]) + self.assertEqual(args["validators"], [_validator]) # defining some non-mandatory arguments in a service should make # them available on further calls to get_arguments. @@ -121,6 +119,7 @@ def test_view_registration(self): def view(request): pass + service.add_view("post", view, validators=(_validator,)) self.assertEqual(len(service.definitions), 1) method, _view, _ = service.definitions[0] @@ -131,15 +130,14 @@ def view(request): def test_error_handler(self): error_handler = object() - service = Service("color", "/favorite-color", - error_handler=error_handler) + service = Service("color", "/favorite-color", error_handler=error_handler) @service.get() def get_favorite_color(request): return "blue, hmm, red, hmm, aaaaaaaah" method, view, args = service.definitions[0] - self.assertIs(args['error_handler'], error_handler) + self.assertIs(args["error_handler"], error_handler) def test_default_error_handler(self): # If not configured otherwise, Service.default_error_handler should @@ -151,7 +149,7 @@ def get_error(request): return "error" method, view, args = service.definitions[0] - self.assertEqual(args['error_handler'], service.default_error_handler) + self.assertEqual(args["error_handler"], service.default_error_handler) def test_default_error_handler_calls_default_renderer(self): # Default error handler should call `render_errors` on the @@ -164,10 +162,8 @@ def test_default_error_handler_calls_default_renderer(self): request = mock.MagicMock() request.registry.queryUtility.return_value = renderer - self.assertEqual(service.default_error_handler(request), - "rendered_errors") - request.registry.queryUtility.assert_called_with(IRendererFactory, - name=service.renderer) + self.assertEqual(service.default_error_handler(request), "rendered_errors") + request.registry.queryUtility.assert_called_with(IRendererFactory, name=service.renderer) renderer.render_errors.assert_called_with(request) def test_decorators(self): @@ -183,8 +179,8 @@ def get_favorite_color(request): method, view, _ = service.definitions[1] self.assertEqual(("HEAD", get_favorite_color), (method, view)) - @service.post(accept='text/plain', renderer='plain') - @service.post(accept='application/json') + @service.post(accept="text/plain", renderer="plain") + @service.post(accept="application/json") def post_favorite_color(request): pass @@ -225,20 +221,17 @@ def test_get_acceptable(self): # to retrieve this information easily service = Service("color", "/favorite-color") service.add_view("GET", lambda x: "blue", accept="text/plain") - self.assertEqual(service.get_acceptable("GET"), ['text/plain']) + self.assertEqual(service.get_acceptable("GET"), ["text/plain"]) service.add_view("GET", lambda x: "blue", accept="application/json") - self.assertEqual(service.get_acceptable("GET"), - ['text/plain', 'application/json']) + self.assertEqual(service.get_acceptable("GET"), ["text/plain", "application/json"]) # adding a view for the POST method should not break everything :-) - service.add_view("POST", lambda x: "ok", accept=('foo/bar')) - self.assertEqual(service.get_acceptable("GET"), - ['text/plain', 'application/json']) + service.add_view("POST", lambda x: "ok", accept=("foo/bar")) + self.assertEqual(service.get_acceptable("GET"), ["text/plain", "application/json"]) # and of course the list of accepted egress content-types should be # available for the "POST" as well. - self.assertEqual(service.get_acceptable("POST"), - ['foo/bar']) + self.assertEqual(service.get_acceptable("POST"), ["foo/bar"]) # it is possible to give acceptable egress content-types dynamically at # run-time. You don't always want to have the callables when retrieving @@ -252,27 +245,22 @@ def test_get_contenttypes(self): # be able to retrieve this information easily service = Service("color", "/favorite-color") service.add_view("GET", lambda x: "blue", content_type="text/plain") - self.assertEqual(service.get_contenttypes("GET"), ['text/plain']) + self.assertEqual(service.get_contenttypes("GET"), ["text/plain"]) - service.add_view("GET", lambda x: "blue", - content_type="application/json") - self.assertEqual(service.get_contenttypes("GET"), - ['text/plain', 'application/json']) + service.add_view("GET", lambda x: "blue", content_type="application/json") + self.assertEqual(service.get_contenttypes("GET"), ["text/plain", "application/json"]) # adding a view for the POST method should not break everything :-) - service.add_view("POST", lambda x: "ok", content_type=('foo/bar')) - self.assertEqual(service.get_contenttypes("GET"), - ['text/plain', 'application/json']) + service.add_view("POST", lambda x: "ok", content_type=("foo/bar")) + self.assertEqual(service.get_contenttypes("GET"), ["text/plain", "application/json"]) # and of course the list of supported ingress content-types should be # available for the "POST" as well. - self.assertEqual(service.get_contenttypes("POST"), - ['foo/bar']) + self.assertEqual(service.get_contenttypes("POST"), ["foo/bar"]) # it is possible to give supported ingress content-types dynamically at # run-time. You don't always want to have the callables when retrieving # all the supported content-types - service.add_view("POST", lambda x: "ok", - content_type=lambda r: "application/json") + service.add_view("POST", lambda x: "ok", content_type=lambda r: "application/json") self.assertEqual(len(service.get_contenttypes("POST")), 2) self.assertEqual(len(service.get_contenttypes("POST", True)), 1) @@ -288,12 +276,10 @@ def validator(request): def validator2(request): pass - service = Service('/color', '/favorite-color') - service.add_view('GET', lambda x: 'ok', - validators=(validator, validator)) - service.add_view('GET', lambda x: 'ok', validators=(validator2)) - self.assertEqual(service.get_validators('GET'), - [validator, validator2]) + service = Service("/color", "/favorite-color") + service.add_view("GET", lambda x: "ok", validators=(validator, validator)) + service.add_view("GET", lambda x: "ok", validators=(validator2)) + self.assertEqual(service.get_validators("GET"), [validator, validator2]) def test_class_parameters(self): # when passing a "klass" argument, it gets registered. It also tests @@ -301,8 +287,8 @@ def test_class_parameters(self): class TemperatureCooler(object): def get_fresh_air(self): pass - service = Service("TemperatureCooler", "/freshair", - klass=TemperatureCooler) + + service = Service("TemperatureCooler", "/freshair", klass=TemperatureCooler) service.add_view("get", "get_fresh_air") self.assertEqual(len(service.definitions), 2) @@ -319,16 +305,33 @@ def test_get_services(self): barbaz = Service("Barbaz", "/barbaz") self.assertIn(barbaz, get_services()) - self.assertEqual([barbaz, ], get_services(exclude=['Foobar', ])) - self.assertEqual([foobar, ], get_services(names=['Foobar', ])) - self.assertEqual([foobar, barbaz], - get_services(names=['Foobar', 'Barbaz'])) + self.assertEqual( + [ + barbaz, + ], + get_services( + exclude=[ + "Foobar", + ] + ), + ) + self.assertEqual( + [ + foobar, + ], + get_services( + names=[ + "Foobar", + ] + ), + ) + self.assertEqual([foobar, barbaz], get_services(names=["Foobar", "Barbaz"])) def test_default_validators(self): - old_validators = Service.default_validators old_filters = Service.default_filters try: + def custom_validator(request): pass @@ -339,14 +342,18 @@ def freshair(request): pass # the default validators should be used when registering a service - Service.default_validators = [custom_validator, ] - Service.default_filters = [custom_filter, ] + Service.default_validators = [ + custom_validator, + ] + Service.default_filters = [ + custom_filter, + ] service = Service("TemperatureCooler", "/freshair") service.add_view("GET", freshair) method, view, args = service.definitions[0] - self.assertIn(custom_validator, args['validators']) - self.assertIn(custom_filter, args['filters']) + self.assertIn(custom_validator, args["validators"]) + self.assertIn(custom_filter, args["filters"]) # defining a service with additional filters / validators should # work as well @@ -359,246 +366,233 @@ def another_filter(request): def groove_em_all(request): pass - service2 = Service('FunkyGroovy', '/funky-groovy', - validators=[another_validator], - filters=[another_filter]) + service2 = Service( + "FunkyGroovy", + "/funky-groovy", + validators=[another_validator], + filters=[another_filter], + ) service2.add_view("GET", groove_em_all) method, view, args = service2.definitions[0] - self.assertIn(custom_validator, args['validators']) - self.assertIn(another_validator, args['validators']) - self.assertIn(custom_filter, args['filters']) - self.assertIn(another_filter, args['filters']) + self.assertIn(custom_validator, args["validators"]) + self.assertIn(another_validator, args["validators"]) + self.assertIn(custom_filter, args["filters"]) + self.assertIn(another_filter, args["filters"]) finally: Service.default_validators = old_validators Service.default_filters = old_filters def test_cors_support(self): - self.assertFalse( - Service(name='foo', path='/foo').cors_enabled) + self.assertFalse(Service(name="foo", path="/foo").cors_enabled) - self.assertTrue( - Service(name='foo', path='/foo', cors_enabled=True) - .cors_enabled) + self.assertTrue(Service(name="foo", path="/foo", cors_enabled=True).cors_enabled) - self.assertFalse( - Service(name='foo', path='/foo', cors_enabled=False) - .cors_enabled) + self.assertFalse(Service(name="foo", path="/foo", cors_enabled=False).cors_enabled) - self.assertTrue( - Service(name='foo', path='/foo', cors_origins=('*',)) - .cors_enabled) + self.assertTrue(Service(name="foo", path="/foo", cors_origins=("*",)).cors_enabled) self.assertFalse( - Service(name='foo', path='/foo', - cors_origins=('*'), cors_enabled=False) - .cors_enabled) + Service(name="foo", path="/foo", cors_origins=("*"), cors_enabled=False).cors_enabled + ) def test_cors_headers_for_service_instantiation(self): # When defining services, it's possible to add headers. This tests # it is possible to list all the headers supported by a service. - service = Service('coconuts', '/migrate', - cors_headers=('X-Header-Coconut')) - self.assertNotIn('X-Header-Coconut', - service.cors_supported_headers_for()) + service = Service("coconuts", "/migrate", cors_headers=("X-Header-Coconut")) + self.assertNotIn("X-Header-Coconut", service.cors_supported_headers_for()) - service.add_view('POST', _stub) - self.assertIn('X-Header-Coconut', service.cors_supported_headers_for()) + service.add_view("POST", _stub) + self.assertIn("X-Header-Coconut", service.cors_supported_headers_for()) def test_cors_headers_for_view_definition(self): # defining headers in the view should work. - service = Service('coconuts', '/migrate') - service.add_view('POST', _stub, cors_headers=('X-Header-Foobar')) - self.assertIn('X-Header-Foobar', service.cors_supported_headers_for()) + service = Service("coconuts", "/migrate") + service.add_view("POST", _stub, cors_headers=("X-Header-Foobar")) + self.assertIn("X-Header-Foobar", service.cors_supported_headers_for()) def test_cors_headers_extension(self): # defining headers in the service and in the view - service = Service('coconuts', '/migrate', - cors_headers=('X-Header-Foobar')) - service.add_view('POST', _stub, cors_headers=('X-Header-Barbaz')) - self.assertIn('X-Header-Foobar', service.cors_supported_headers_for()) - self.assertIn('X-Header-Barbaz', service.cors_supported_headers_for()) + service = Service("coconuts", "/migrate", cors_headers=("X-Header-Foobar")) + service.add_view("POST", _stub, cors_headers=("X-Header-Barbaz")) + self.assertIn("X-Header-Foobar", service.cors_supported_headers_for()) + self.assertIn("X-Header-Barbaz", service.cors_supported_headers_for()) # check that adding the same header twice doesn't make bad things # happen - service.add_view('POST', _stub, cors_headers=('X-Header-Foobar'),) + service.add_view( + "POST", + _stub, + cors_headers=("X-Header-Foobar"), + ) self.assertEqual(len(service.cors_supported_headers_for()), 2) # check that adding a header on a cors disabled method doesn't # change anything - service.add_view('put', _stub, - cors_headers=('X-Another-Header',), - cors_enabled=False) + service.add_view("put", _stub, cors_headers=("X-Another-Header",), cors_enabled=False) - self.assertNotIn('X-Another-Header', - service.cors_supported_headers_for()) + self.assertNotIn("X-Another-Header", service.cors_supported_headers_for()) def test_cors_headers_for_method(self): # defining headers in the view should work. - service = Service('coconuts', '/migrate') - service.add_view('GET', _stub, cors_headers=('X-Header-Foobar')) - service.add_view('POST', _stub, cors_headers=('X-Header-Barbaz')) - get_headers = service.cors_supported_headers_for(method='GET') - self.assertNotIn('X-Header-Barbaz', get_headers) + service = Service("coconuts", "/migrate") + service.add_view("GET", _stub, cors_headers=("X-Header-Foobar")) + service.add_view("POST", _stub, cors_headers=("X-Header-Barbaz")) + get_headers = service.cors_supported_headers_for(method="GET") + self.assertNotIn("X-Header-Barbaz", get_headers) def test_cors_headers_for_method_are_deduplicated(self): # defining headers in the view should work. - service = Service('coconuts', '/migrate') - service.cors_headers = ('X-Header-Foobar',) - service.add_view('GET', _stub, - cors_headers=('X-Header-Foobar', 'X-Header-Barbaz')) - get_headers = service.cors_supported_headers_for(method='GET') - expected = set(['X-Header-Foobar', 'X-Header-Barbaz']) + service = Service("coconuts", "/migrate") + service.cors_headers = ("X-Header-Foobar",) + service.add_view("GET", _stub, cors_headers=("X-Header-Foobar", "X-Header-Barbaz")) + get_headers = service.cors_supported_headers_for(method="GET") + expected = set(["X-Header-Foobar", "X-Header-Barbaz"]) self.assertEqual(expected, get_headers) def test_cors_supported_methods(self): - foo = Service(name='foo', path='/foo', cors_enabled=True) - foo.add_view('GET', _stub) - self.assertIn('GET', foo.cors_supported_methods) + foo = Service(name="foo", path="/foo", cors_enabled=True) + foo.add_view("GET", _stub) + self.assertIn("GET", foo.cors_supported_methods) - foo.add_view('POST', _stub) - self.assertIn('POST', foo.cors_supported_methods) + foo.add_view("POST", _stub) + self.assertIn("POST", foo.cors_supported_methods) def test_disabling_cors_for_one_method(self): - foo = Service(name='foo', path='/foo', cors_enabled=True) - foo.add_view('GET', _stub) - self.assertIn('GET', foo.cors_supported_methods) + foo = Service(name="foo", path="/foo", cors_enabled=True) + foo.add_view("GET", _stub) + self.assertIn("GET", foo.cors_supported_methods) - foo.add_view('POST', _stub, cors_enabled=False) - self.assertIn('GET', foo.cors_supported_methods) - self.assertFalse('POST' in foo.cors_supported_methods) + foo.add_view("POST", _stub, cors_enabled=False) + self.assertIn("GET", foo.cors_supported_methods) + self.assertFalse("POST" in foo.cors_supported_methods) def test_cors_supported_origins(self): - foo = Service( - name='foo', path='/foo', cors_origins=('mozilla.org',)) + foo = Service(name="foo", path="/foo", cors_origins=("mozilla.org",)) - foo.add_view('GET', _stub, - cors_origins=('notmyidea.org', 'lolnet.org')) + foo.add_view("GET", _stub, cors_origins=("notmyidea.org", "lolnet.org")) - self.assertIn('mozilla.org', foo.cors_supported_origins) - self.assertIn('notmyidea.org', foo.cors_supported_origins) - self.assertIn('lolnet.org', foo.cors_supported_origins) + self.assertIn("mozilla.org", foo.cors_supported_origins) + self.assertIn("notmyidea.org", foo.cors_supported_origins) + self.assertIn("lolnet.org", foo.cors_supported_origins) def test_per_method_supported_origins(self): - foo = Service( - name='foo', path='/foo', cors_origins=('mozilla.org',)) - foo.add_view('GET', _stub, cors_origins=('lolnet.org',)) + foo = Service(name="foo", path="/foo", cors_origins=("mozilla.org",)) + foo.add_view("GET", _stub, cors_origins=("lolnet.org",)) - self.assertTrue('mozilla.org' in foo.cors_origins_for('GET')) - self.assertTrue('lolnet.org' in foo.cors_origins_for('GET')) + self.assertTrue("mozilla.org" in foo.cors_origins_for("GET")) + self.assertTrue("lolnet.org" in foo.cors_origins_for("GET")) - foo.add_view('POST', _stub) - self.assertFalse('lolnet.org' in foo.cors_origins_for('POST')) + foo.add_view("POST", _stub) + self.assertFalse("lolnet.org" in foo.cors_origins_for("POST")) def test_credential_support_can_be_enabled(self): - foo = Service(name='foo', path='/foo', cors_credentials=True) - foo.add_view('POST', _stub) + foo = Service(name="foo", path="/foo", cors_credentials=True) + foo.add_view("POST", _stub) self.assertTrue(foo.cors_support_credentials_for()) def test_credential_support_is_disabled_by_default(self): - foo = Service(name='foo', path='/foo') - foo.add_view('POST', _stub) + foo = Service(name="foo", path="/foo") + foo.add_view("POST", _stub) self.assertFalse(foo.cors_support_credentials_for()) def test_per_method_credential_support(self): - foo = Service(name='foo', path='/foo') - foo.add_view('GET', _stub, cors_credentials=True) - foo.add_view('POST', _stub) - self.assertTrue(foo.cors_support_credentials_for('GET')) - self.assertFalse(foo.cors_support_credentials_for('POST')) + foo = Service(name="foo", path="/foo") + foo.add_view("GET", _stub, cors_credentials=True) + foo.add_view("POST", _stub) + self.assertTrue(foo.cors_support_credentials_for("GET")) + self.assertFalse(foo.cors_support_credentials_for("POST")) def test_method_takes_precendence_for_credential_support(self): - foo = Service(name='foo', path='/foo', cors_credentials=True) - foo.add_view('GET', _stub, cors_credentials=False) - self.assertFalse(foo.cors_support_credentials_for('GET')) + foo = Service(name="foo", path="/foo", cors_credentials=True) + foo.add_view("GET", _stub, cors_credentials=False) + self.assertFalse(foo.cors_support_credentials_for("GET")) def test_max_age_is_none_if_undefined(self): - foo = Service(name='foo', path='/foo') - foo.add_view('POST', _stub) - self.assertIsNone(foo.cors_max_age_for('POST')) + foo = Service(name="foo", path="/foo") + foo.add_view("POST", _stub) + self.assertIsNone(foo.cors_max_age_for("POST")) def test_max_age_can_be_defined(self): - foo = Service(name='foo', path='/foo', cors_max_age=42) - foo.add_view('POST', _stub) + foo = Service(name="foo", path="/foo", cors_max_age=42) + foo.add_view("POST", _stub) self.assertEqual(foo.cors_max_age_for(), 42) def test_max_age_can_be_different_dependeing_methods(self): - foo = Service(name='foo', path='/foo', cors_max_age=42) - foo.add_view('GET', _stub) - foo.add_view('POST', _stub, cors_max_age=32) - foo.add_view('PUT', _stub, cors_max_age=7) + foo = Service(name="foo", path="/foo", cors_max_age=42) + foo.add_view("GET", _stub) + foo.add_view("POST", _stub, cors_max_age=32) + foo.add_view("PUT", _stub, cors_max_age=7) - self.assertEqual(foo.cors_max_age_for('GET'), 42) - self.assertEqual(foo.cors_max_age_for('POST'), 32) - self.assertEqual(foo.cors_max_age_for('PUT'), 7) + self.assertEqual(foo.cors_max_age_for("GET"), 42) + self.assertEqual(foo.cors_max_age_for("POST"), 32) + self.assertEqual(foo.cors_max_age_for("PUT"), 7) def test_cors_policy(self): - policy = {'origins': ('foo', 'bar', 'baz')} - foo = Service(name='foo', path='/foo', cors_policy=policy) - self.assertTrue('foo' in foo.cors_supported_origins) - self.assertTrue('bar' in foo.cors_supported_origins) - self.assertTrue('baz' in foo.cors_supported_origins) + policy = {"origins": ("foo", "bar", "baz")} + foo = Service(name="foo", path="/foo", cors_policy=policy) + self.assertTrue("foo" in foo.cors_supported_origins) + self.assertTrue("bar" in foo.cors_supported_origins) + self.assertTrue("baz" in foo.cors_supported_origins) def test_cors_policy_can_be_overwritten(self): - policy = {'origins': ('foo', 'bar', 'baz')} - foo = Service(name='foo', path='/foo', cors_origins=(), - cors_policy=policy) + policy = {"origins": ("foo", "bar", "baz")} + foo = Service(name="foo", path="/foo", cors_origins=(), cors_policy=policy) self.assertEqual(len(foo.cors_supported_origins), 0) def test_can_specify_a_view_decorator(self): def dummy_decorator(view): return view + service = Service("coconuts", "/migrate", decorator=dummy_decorator) args = service.get_arguments({}) - self.assertEqual(args['decorator'], dummy_decorator) + self.assertEqual(args["decorator"], dummy_decorator) # make sure Service.decorator() still works - @service.decorator('put') + @service.decorator("put") def dummy_view(request): return "data" - self.assertTrue(any(view is dummy_view - for method, view, args in service.definitions)) - def test_decorate_view_factory(self): + self.assertTrue(any(view is dummy_view for method, view, args in service.definitions)) - args = {'klass': DummyAPI} - route_args = {'factory': u'TheFactoryMethodCalledByPyramid'} + def test_decorate_view_factory(self): + args = {"klass": DummyAPI} + route_args = {"factory": "TheFactoryMethodCalledByPyramid"} - decorated_view = decorate_view('collection_get', args, - 'GET', route_args) + decorated_view = decorate_view("collection_get", args, "GET", route_args) dummy_request = DummyRequest() ret = decorated_view(dummy_request) - self.assertEqual(ret, ['douggy', 'rusty']) + self.assertEqual(ret, ["douggy", "rusty"]) self.assertEqual(dummy_request, DummyAPI.last_request) self.assertEqual(dummy_request.context, DummyAPI.last_context) def test_decorate_view_acl(self): + args = {"acl": "dummy_permission", "klass": DummyAPI} - args = {'acl': 'dummy_permission', - 'klass': DummyAPI} - - decorated_view = decorate_view('collection_get', args, 'GET') + decorated_view = decorate_view("collection_get", args, "GET") dummy_request = DummyRequest() ret = decorated_view(dummy_request) - self.assertEqual(ret, ['douggy', 'rusty']) + self.assertEqual(ret, ["douggy", "rusty"]) self.assertEqual(dummy_request, DummyAPI.last_request) self.assertIsNone(DummyAPI.last_context) def test_cannot_specify_both_factory_and_acl(self): with self.assertRaises(ConfigurationError): - Service("coconuts", "/migrate", - acl='dummy_permission', - factory='TheFactoryMethodCalledByPyramid') + Service( + "coconuts", + "/migrate", + acl="dummy_permission", + factory="TheFactoryMethodCalledByPyramid", + ) def test_decorate_view(self): def myfunction(): pass - meth = 'POST' + meth = "POST" decorated = decorate_view(myfunction, {}, meth) - self.assertEqual(decorated.__name__, "{0}__{1}".format( - func_name(myfunction), meth)) + self.assertEqual(decorated.__name__, "{0}__{1}".format(func_name(myfunction), meth)) def test_decorate_resource_view(self): class MyResource(object): @@ -608,7 +602,6 @@ def __init__(self, **kwargs): def myview(self): pass - meth = 'POST' - decorated = decorate_view(_UnboundView(MyResource, 'myview'), {}, meth) - self.assertEqual(decorated.__name__, "{0}__{1}".format( - func_name(MyResource.myview), meth)) + meth = "POST" + decorated = decorate_view(_UnboundView(MyResource, "myview"), {}, meth) + self.assertEqual(decorated.__name__, "{0}__{1}".format(func_name(MyResource.myview), meth)) diff --git a/tests/test_service_definition.py b/tests/test_service_definition.py index 98204e1c..db943dcc 100644 --- a/tests/test_service_definition.py +++ b/tests/test_service_definition.py @@ -2,12 +2,11 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. +from cornice import Service from pyramid import testing from webtest import TestApp -from cornice import Service - -from .support import TestCase, CatchErrors +from .support import CatchErrors, TestCase service1 = Service(name="service1", path="/service1") @@ -31,7 +30,6 @@ def get2_or_post2(request): class TestServiceDefinition(TestCase): - def setUp(self): self.config = testing.setUp() self.config.include("cornice") @@ -42,15 +40,10 @@ def tearDown(self): testing.tearDown() def test_basic_service_operation(self): - self.app.get("/unknown", status=404) - self.assertEqual( - self.app.get("/service1").json, - {'test': "succeeded"}) + self.assertEqual(self.app.get("/service1").json, {"test": "succeeded"}) - self.assertEqual( - self.app.post("/service1", params="BODY").json, - {'body': 'BODY'}) + self.assertEqual(self.app.post("/service1", params="BODY").json, {"body": "BODY"}) def test_loading_into_multiple_configurators(self): # When initializing a second configurator, it shouldn't interfere @@ -61,20 +54,16 @@ def test_loading_into_multiple_configurators(self): # Calling the new configurator works as expected. app = TestApp(CatchErrors(config2.make_wsgi_app())) - self.assertEqual( - app.get("/service1").json, - {'test': 'succeeded'}) + self.assertEqual(app.get("/service1").json, {"test": "succeeded"}) # Calling the old configurator works as expected. - self.assertEqual( - self.app.get("/service1").json, - {'test': 'succeeded'}) + self.assertEqual(self.app.get("/service1").json, {"test": "succeeded"}) def test_stacking_api_decorators(self): # Stacking multiple @api calls on a single function should # register it multiple times, just like @view_config does. - resp = self.app.get("/service2", headers={'Accept': 'text/html'}) - self.assertEqual(resp.json, {'test': 'succeeded'}) + resp = self.app.get("/service2", headers={"Accept": "text/html"}) + self.assertEqual(resp.json, {"test": "succeeded"}) - resp = self.app.post("/service2", headers={'Accept': 'audio/ogg'}) - self.assertEqual(resp.json, {'test': 'succeeded'}) + resp = self.app.post("/service2", headers={"Accept": "audio/ogg"}) + self.assertEqual(resp.json, {"test": "succeeded"}) diff --git a/tests/test_util.py b/tests/test_util.py index 42f9f569..d01055bc 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -6,30 +6,28 @@ class TestDeprecatedUtils(unittest.TestCase): - def test_extract_json_data_is_deprecated(self): - with mock.patch('cornice.util.warnings') as mocked: + with mock.patch("cornice.util.warnings") as mocked: util.extract_json_data(mock.MagicMock()) self.assertTrue(mocked.warn.called) def test_extract_form_urlencoded_data_is_deprecated(self): - with mock.patch('cornice.util.warnings') as mocked: + with mock.patch("cornice.util.warnings") as mocked: util.extract_form_urlencoded_data(mock.MagicMock()) self.assertTrue(mocked.warn.called) class CurrentServiceTest(unittest.TestCase): - def test_current_service_returns_the_service_for_existing_patterns(self): request = mock.MagicMock() - request.matched_route.pattern = '/buckets' - request.registry.cornice_services = {'/buckets': mock.sentinel.service} + request.matched_route.pattern = "/buckets" + request.registry.cornice_services = {"/buckets": mock.sentinel.service} self.assertEqual(util.current_service(request), mock.sentinel.service) def test_current_service_returns_none_for_unexisting_patterns(self): request = mock.MagicMock() - request.matched_route.pattern = '/unexisting' + request.matched_route.pattern = "/unexisting" request.registry.cornice_services = {} self.assertEqual(util.current_service(request), None) diff --git a/tests/test_validation.py b/tests/test_validation.py index 37897ae6..96c325ed 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -9,59 +9,62 @@ from pyramid.request import Request from webtest import TestApp + + try: import colander + COLANDER = True except ImportError: COLANDER = False try: - import marshmallow + import marshmallow # noqa + MARSHMALLOW = True except ImportError: MARSHMALLOW = False from cornice.errors import Errors -from cornice.validators import (colander_validator, colander_body_validator, - extract_cstruct) - -from cornice.validators import (marshmallow_validator, - marshmallow_body_validator) - +from cornice.validators import ( + colander_body_validator, + colander_validator, + extract_cstruct, + marshmallow_body_validator, + marshmallow_validator, +) + +from .support import DummyRequest, LoggingCatcher, TestCase from .validationapp import main -from .support import LoggingCatcher, TestCase, DummyRequest -skip_if_no_colander = unittest.skipIf(COLANDER is False, - "colander is not installed.") +skip_if_no_colander = unittest.skipIf(COLANDER is False, "colander is not installed.") -skip_if_no_marshmallow = unittest.skipIf(MARSHMALLOW is False, - "marshmallow is not installed.") +skip_if_no_marshmallow = unittest.skipIf(MARSHMALLOW is False, "marshmallow is not installed.") @skip_if_no_colander class TestServiceDefinition(LoggingCatcher, TestCase): - def test_validation(self): app = TestApp(main({})) - app.get('/service', status=400) + app.get("/service", status=400) - response = app.post('/service', params='buh', status=400) - self.assertTrue(b'Not a json body' in response.body) + response = app.post("/service", params="buh", status=400) + self.assertTrue(b"Not a json body" in response.body) - response = app.post('/service', params=json.dumps('buh')) + response = app.post("/service", params=json.dumps("buh")) - expected = json.dumps({'body': '"buh"'}).encode('ascii') + expected = json.dumps({"body": '"buh"'}).encode("ascii") self.assertEqual(response.body, expected) - app.get('/service?paid=yup') + app.get("/service?paid=yup") # valid = foo is one - response = app.get('/service?foo=1&paid=yup') - self.assertEqual(response.json['foo'], 1) + response = app.get("/service?foo=1&paid=yup") + self.assertEqual(response.json["foo"], 1) # invalid value for foo - response = app.get('/service?foo=buh&paid=yup', status=400) + response = app.get("/service?foo=buh&paid=yup", status=400) # check that json is returned errors = Errors.from_json(response.body) @@ -70,89 +73,83 @@ def test_validation(self): def test_validation_hooked_error_response(self): app = TestApp(main({})) - response = app.post('/service4', status=400) - self.assertTrue(b'' in response.body) + response = app.post("/service4", status=400) + self.assertTrue(b"" in response.body) def test_accept(self): # tests that the accept headers are handled the proper way app = TestApp(main({})) # requesting the wrong accept header should return a 406 ... - response = app.get('/service2', headers={'Accept': 'audio/*'}, - status=406) + response = app.get("/service2", headers={"Accept": "audio/*"}, status=406) # ... with the list of accepted content-types - error_location = response.json['errors'][0]['location'] - error_name = response.json['errors'][0]['name'] - error_description = response.json['errors'][0]['description'] - self.assertEqual('header', error_location) - self.assertEqual('Accept', error_name) - self.assertIn('application/json', error_description) - self.assertIn('text/plain', error_description) + error_location = response.json["errors"][0]["location"] + error_name = response.json["errors"][0]["name"] + error_description = response.json["errors"][0]["description"] + self.assertEqual("header", error_location) + self.assertEqual("Accept", error_name) + self.assertIn("application/json", error_description) + self.assertIn("text/plain", error_description) # requesting a supported type should give an appropriate response type - response = app.get('/service2', headers={'Accept': 'application/*'}) + response = app.get("/service2", headers={"Accept": "application/*"}) self.assertEqual(response.content_type, "application/json") - response = app.get('/service2', headers={'Accept': 'text/plain'}) + response = app.get("/service2", headers={"Accept": "text/plain"}) self.assertEqual(response.content_type, "text/plain") # it should also work with multiple Accept headers - response = app.get('/service2', headers={ - 'Accept': 'audio/*, application/*' - }) + response = app.get("/service2", headers={"Accept": "audio/*, application/*"}) self.assertEqual(response.content_type, "application/json") # and requested preference order should be respected - headers = {'Accept': 'application/json; q=1.0, text/plain; q=0.9'} - response = app.get('/service2', headers=headers) + headers = {"Accept": "application/json; q=1.0, text/plain; q=0.9"} + response = app.get("/service2", headers=headers) self.assertEqual(response.content_type, "application/json") - headers = {'Accept': 'application/json; q=0.9, text/plain; q=1.0'} - response = app.get('/service2', headers=headers) + headers = {"Accept": "application/json; q=0.9, text/plain; q=1.0"} + response = app.get("/service2", headers=headers) self.assertEqual(response.content_type, "text/plain") # test that using a callable to define what's accepted works as well - response = app.get('/service3', headers={'Accept': 'audio/*'}, - status=406) - error_description = response.json['errors'][0]['description'] - self.assertIn('application/json', error_description) + response = app.get("/service3", headers={"Accept": "audio/*"}, status=406) + error_description = response.json["errors"][0]["description"] + self.assertIn("application/json", error_description) - response = app.get('/service3', headers={'Accept': 'text/*'}) + response = app.get("/service3", headers={"Accept": "text/*"}) self.assertEqual(response.content_type, "text/plain") # Test that using a callable to define what's accepted works as well. # Now, the callable returns a scalar instead of a list. - response = app.put('/service3', headers={'Accept': 'audio/*'}, - status=406) - error_description = response.json['errors'][0]['description'] - self.assertIn('application/json', error_description) + response = app.put("/service3", headers={"Accept": "audio/*"}, status=406) + error_description = response.json["errors"][0]["description"] + self.assertIn("application/json", error_description) - response = app.put('/service3', headers={'Accept': 'text/*'}) + response = app.put("/service3", headers={"Accept": "text/*"}) self.assertEqual(response.content_type, "text/plain") # If we are not asking for a particular content-type, # we should get one of the two types that the service supports. - response = app.get('/service2') - self.assertIn(response.content_type, - ("application/json", "text/plain")) + response = app.get("/service2") + self.assertIn(response.content_type, ("application/json", "text/plain")) def test_accept_issue_113_text_star(self): app = TestApp(main({})) - response = app.get('/service3', headers={'Accept': 'text/*'}) + response = app.get("/service3", headers={"Accept": "text/*"}) self.assertEqual(response.content_type, "text/plain") def test_accept_issue_113_text_application_star(self): app = TestApp(main({})) - response = app.get('/service3', headers={'Accept': 'application/*'}) + response = app.get("/service3", headers={"Accept": "application/*"}) self.assertEqual(response.content_type, "application/json") def test_accept_issue_113_text_application_json(self): app = TestApp(main({})) - response = app.get('/service3', headers={'Accept': 'application/json'}) + response = app.get("/service3", headers={"Accept": "application/json"}) self.assertEqual(response.content_type, "application/json") def test_accept_issue_113_text_html_not_acceptable(self): @@ -160,31 +157,29 @@ def test_accept_issue_113_text_html_not_acceptable(self): # Requesting an unsupported content type should # return HTTP response "406 Not Acceptable". - app.get('/service3', headers={'Accept': 'text/html'}, status=406) + app.get("/service3", headers={"Accept": "text/html"}, status=406) def test_accept_issue_113_audio_or_text(self): app = TestApp(main({})) - response = app.get('/service2', headers={ - 'Accept': 'audio/mp4; q=0.9, text/plain; q=0.5' - }) + response = app.get("/service2", headers={"Accept": "audio/mp4; q=0.9, text/plain; q=0.5"}) self.assertEqual(response.content_type, "text/plain") # If we are not asking for a particular content-type, # we should get one of the two types that the service supports. - response = app.get('/service2') - self.assertIn(response.content_type, - ("application/json", "text/plain")) + response = app.get("/service2") + self.assertIn(response.content_type, ("application/json", "text/plain")) def test_override_default_accept_issue_252(self): # Override default acceptable content_types for interoperate with # legacy applications i.e. ExtJS 3. from cornice.renderer import CorniceRenderer - CorniceRenderer.acceptable += ('text/html',) + + CorniceRenderer.acceptable += ("text/html",) app = TestApp(main({})) - response = app.get('/service5', headers={'Accept': 'text/html'}) + response = app.get("/service5", headers={"Accept": "text/html"}) self.assertEqual(response.content_type, "text/html") # revert the override CorniceRenderer.acceptable = CorniceRenderer.acceptable[:-1] @@ -193,17 +188,16 @@ def test_filters(self): app = TestApp(main({})) # filters can be applied to all the methods of a service - self.assertTrue(b"filtered response" in app.get('/filtered').body) - self.assertTrue(b"unfiltered" in app.post('/filtered').body) + self.assertTrue(b"filtered response" in app.get("/filtered").body) + self.assertTrue(b"unfiltered" in app.post("/filtered").body) def test_multiple_querystrings(self): app = TestApp(main({})) # it is possible to have more than one value with the same name in the # querystring - self.assertEqual(b'{"field": ["5"]}', app.get('/foobaz?field=5').body) - self.assertEqual(b'{"field": ["5", "2"]}', - app.get('/foobaz?field=5&field=2').body) + self.assertEqual(b'{"field": ["5"]}', app.get("/foobaz?field=5").body) + self.assertEqual(b'{"field": ["5", "2"]}', app.get("/foobaz?field=5&field=2").body) def test_content_type_missing(self): # test that a Content-Type request headers is present @@ -211,75 +205,69 @@ def test_content_type_missing(self): # Requesting without a Content-Type header should # return "415 Unsupported Media Type" ... - request = app.RequestClass.blank('/service5', method='POST', - POST="some data") + request = app.RequestClass.blank("/service5", method="POST", POST="some data") response = app.do_request(request, 415, True) self.assertEqual(response.status_code, 415) # ... with an appropriate json error structure. - error_location = response.json['errors'][0]['location'] - error_name = response.json['errors'][0]['name'] - error_description = response.json['errors'][0]['description'] - self.assertEqual('header', error_location) - self.assertEqual('Content-Type', error_name) - self.assertIn('application/json', error_description) + error_location = response.json["errors"][0]["location"] + error_name = response.json["errors"][0]["name"] + error_description = response.json["errors"][0]["description"] + self.assertEqual("header", error_location) + self.assertEqual("Content-Type", error_name) + self.assertIn("application/json", error_description) def test_validated_body_content_from_schema(self): app = TestApp(main({})) - content = {'email': 'alexis@notmyidea.org'} - response = app.post_json('/newsletter', params=content) + content = {"email": "alexis@notmyidea.org"} + response = app.post_json("/newsletter", params=content) self.assertEqual(response.status_code, 200) - self.assertEqual(response.json['body'], content) + self.assertEqual(response.json["body"], content) def test_validated_querystring_content_from_schema(self): app = TestApp(main({})) - response = app.post_json('/newsletter?ref=3') + response = app.post_json("/newsletter?ref=3") self.assertEqual(response.status_code, 200) - self.assertEqual(response.json['querystring'], {"ref": 3}) + self.assertEqual(response.json["querystring"], {"ref": 3}) def test_validated_querystring_and_schema_from_same_schema(self): app = TestApp(main({})) - content = {'email': 'alexis@notmyidea.org'} - response = app.post_json('/newsletter?ref=20', params=content) + content = {"email": "alexis@notmyidea.org"} + response = app.post_json("/newsletter?ref=20", params=content) self.assertEqual(response.status_code, 200) - self.assertEqual(response.json['body'], content) - self.assertEqual(response.json['querystring'], {"ref": 20}) + self.assertEqual(response.json["body"], content) + self.assertEqual(response.json["querystring"], {"ref": 20}) - response = app.post_json('/newsletter?ref=2', params=content, - status=400) + response = app.post_json("/newsletter?ref=2", params=content, status=400) self.assertEqual(response.status_code, 400) - error = { - 'location': 'body', - 'name': 'email', - 'description': 'Invalid email length' - } - self.assertEqual(response.json['errors'][0], error) + error = {"location": "body", "name": "email", "description": "Invalid email length"} + self.assertEqual(response.json["errors"][0], error) def test_validated_path_content_from_schema(self): # Test validation request.matchdict. (See #411) app = TestApp(main({})) - response = app.get('/item/42', status=200) - self.assertEqual(response.json, {'item_id': 42}) + response = app.get("/item/42", status=200) + self.assertEqual(response.json, {"item_id": 42}) def test_content_type_with_no_body_should_pass(self): app = TestApp(main({})) - request = app.RequestClass.blank('/newsletter', method='POST', - headers={'Content-Type': - 'application/json'}) + request = app.RequestClass.blank( + "/newsletter", method="POST", headers={"Content-Type": "application/json"} + ) response = app.do_request(request, 200, True) self.assertEqual(response.status_code, 200) - self.assertEqual(response.json['body'], {}) + self.assertEqual(response.json["body"], {}) def test_content_type_missing_with_no_body_should_pass(self): app = TestApp(main({})) # requesting without a Content-Type header nor a body should # return a 200. - request = app.RequestClass.blank('/newsletter', method='POST') + request = app.RequestClass.blank("/newsletter", method="POST") response = app.do_request(request, 200, True) self.assertEqual(response.status_code, 200) - self.assertEqual(response.json['body'], {}) + self.assertEqual(response.json["body"], {}) def test_content_type_wrong_single(self): # Tests that the Content-Type request header satisfies the requirement. @@ -287,13 +275,11 @@ def test_content_type_wrong_single(self): # Requesting the wrong Content-Type header should # return "415 Unsupported Media Type" ... - response = app.post('/service5', - headers={'Content-Type': 'text/plain'}, - status=415) + response = app.post("/service5", headers={"Content-Type": "text/plain"}, status=415) # ... with an appropriate json error structure. - error_description = response.json['errors'][0]['description'] - self.assertIn('application/json', error_description) + error_description = response.json["errors"][0]["description"] + self.assertIn("application/json", error_description) def test_content_type_wrong_multiple(self): # Tests that the Content-Type request header satisfies the requirement. @@ -301,14 +287,12 @@ def test_content_type_wrong_multiple(self): # Requesting without a Content-Type header should # return "415 Unsupported Media Type" ... - response = app.put('/service5', - headers={'Content-Type': 'text/xml'}, - status=415) + response = app.put("/service5", headers={"Content-Type": "text/xml"}, status=415) # ... with an appropriate json error structure. - error_description = response.json['errors'][0]['description'] - self.assertIn('text/plain', error_description) - self.assertIn('application/json', error_description) + error_description = response.json["errors"][0]["description"] + self.assertIn("text/plain", error_description) + self.assertIn("application/json", error_description) def test_content_type_correct(self): # Tests that the Content-Type request header satisfies the requirement. @@ -316,35 +300,31 @@ def test_content_type_correct(self): # Requesting with one of the allowed Content-Type headers should work, # even when having a charset parameter as suffix. - response = app.put('/service5', headers={ - 'Content-Type': 'text/plain; charset=utf-8' - }) + response = app.put("/service5", headers={"Content-Type": "text/plain; charset=utf-8"}) self.assertEqual(response.json, "some response") def test_content_type_on_get(self): # Test that a Content-Type request header is not # checked on GET requests, they don't usually have a body. app = TestApp(main({})) - response = app.get('/service5') + response = app.get("/service5") self.assertEqual(response.json, "some response") def test_content_type_with_callable(self): # Test that using a callable for content_type works as well. app = TestApp(main({})) - response = app.post('/service6', headers={'Content-Type': 'audio/*'}, - status=415) - error_description = response.json['errors'][0]['description'] - self.assertIn('text/xml', error_description) - self.assertIn('application/json', error_description) + response = app.post("/service6", headers={"Content-Type": "audio/*"}, status=415) + error_description = response.json["errors"][0]["description"] + self.assertIn("text/xml", error_description) + self.assertIn("application/json", error_description) def test_content_type_with_callable_returning_scalar(self): # Test that using a callable for content_type works as well. # Now, the callable returns a scalar instead of a list. app = TestApp(main({})) - response = app.put('/service6', headers={'Content-Type': 'audio/*'}, - status=415) - error_description = response.json['errors'][0]['description'] - self.assertIn('text/xml', error_description) + response = app.put("/service6", headers={"Content-Type": "audio/*"}, status=415) + error_description = response.json["errors"][0]["description"] + self.assertIn("text/xml", error_description) def test_accept_and_content_type(self): # Tests that using both the "Accept" and "Content-Type" @@ -352,229 +332,228 @@ def test_accept_and_content_type(self): app = TestApp(main({})) # POST endpoint just has one accept and content_type definition - response = app.post('/service7', headers={ - 'Accept': 'text/xml, application/json', - 'Content-Type': 'application/json; charset=utf-8' - }) + response = app.post( + "/service7", + headers={ + "Accept": "text/xml, application/json", + "Content-Type": "application/json; charset=utf-8", + }, + ) self.assertEqual(response.json, "some response") response = app.post( - '/service7', + "/service7", headers={ - 'Accept': 'text/plain, application/json', - 'Content-Type': 'application/json; charset=utf-8' + "Accept": "text/plain, application/json", + "Content-Type": "application/json; charset=utf-8", }, - status=406) + status=406, + ) response = app.post( - '/service7', + "/service7", headers={ - 'Accept': 'text/xml, application/json', - 'Content-Type': 'application/x-www-form-urlencoded' + "Accept": "text/xml, application/json", + "Content-Type": "application/x-www-form-urlencoded", }, - status=415) + status=415, + ) # PUT endpoint has a list of accept and content_type definitions - response = app.put('/service7', headers={ - 'Accept': 'text/xml, application/json', - 'Content-Type': 'application/json; charset=utf-8' - }) - self.assertEqual(response.json, "some response") - response = app.put( - '/service7', + "/service7", headers={ - 'Accept': 'audio/*', - 'Content-Type': 'application/json; charset=utf-8' + "Accept": "text/xml, application/json", + "Content-Type": "application/json; charset=utf-8", }, - status=406) + ) + self.assertEqual(response.json, "some response") + + response = app.put( + "/service7", + headers={"Accept": "audio/*", "Content-Type": "application/json; charset=utf-8"}, + status=406, + ) response = app.put( - '/service7', + "/service7", headers={ - 'Accept': 'text/xml, application/json', - 'Content-Type': 'application/x-www-form-urlencoded' - }, status=415) + "Accept": "text/xml, application/json", + "Content-Type": "application/x-www-form-urlencoded", + }, + status=415, + ) @skip_if_no_colander class TestRequestDataExtractors(LoggingCatcher, TestCase): - def make_ordinary_app(self): return TestApp(main({})) def test_valid_json(self): app = self.make_ordinary_app() - response = app.post_json('/signup', { - 'username': 'man', - }) - self.assertEqual(response.json['username'], 'man') + response = app.post_json( + "/signup", + { + "username": "man", + }, + ) + self.assertEqual(response.json["username"], "man") def test_valid_nonstandard_json(self): app = self.make_ordinary_app() response = app.post_json( - '/signup', - {'username': 'man'}, - headers={'content-type': 'application/merge-patch+json'} + "/signup", + {"username": "man"}, + headers={"content-type": "application/merge-patch+json"}, ) - self.assertEqual(response.json['username'], 'man') + self.assertEqual(response.json["username"], "man") def test_json_array_with_colander_body_validator(self): app = self.make_ordinary_app() with self.assertRaises(TypeError) as context: - app.post_json( - '/group_signup', - [{'username': 'hey'}, {'username': 'how'}] - ) - self.assertIn('Schema should inherit from colander.MappingSchema.', - str(context)) + app.post_json("/group_signup", [{"username": "hey"}, {"username": "how"}]) + self.assertIn("Schema should inherit from colander.MappingSchema.", str(context)) def test_json_body_attribute_is_not_lost(self): app = self.make_ordinary_app() - response = app.post_json( - '/body_signup', - {'body': {'username': 'hey'}} - ) - self.assertEqual(response.json['data'], {'body': {'username': 'hey'}}) + response = app.post_json("/body_signup", {"body": {"username": "hey"}}) + self.assertEqual(response.json["data"], {"body": {"username": "hey"}}) def test_json_array_with_colander_validator(self): app = self.make_ordinary_app() - response = app.post_json( - '/body_group_signup', - [{'username': 'hey'}, {'username': 'how'}] - ) - self.assertEqual(response.json['data'], - [{'username': 'hey'}, {'username': 'how'}]) + response = app.post_json("/body_group_signup", [{"username": "hey"}, {"username": "how"}]) + self.assertEqual(response.json["data"], [{"username": "hey"}, {"username": "how"}]) def test_invalid_json(self): app = self.make_ordinary_app() - response = app.post('/signup', - '{"foo": "bar"', - headers={'content-type': 'application/json'}, - status=400) - self.assertEqual(response.json['status'], 'error') - error_description = response.json['errors'][0]['description'] - self.assertIn('Invalid JSON: Expecting', error_description) + response = app.post( + "/signup", '{"foo": "bar"', headers={"content-type": "application/json"}, status=400 + ) + self.assertEqual(response.json["status"], "error") + error_description = response.json["errors"][0]["description"] + self.assertIn("Invalid JSON: Expecting", error_description) def test_json_text(self): app = self.make_ordinary_app() - response = app.post('/signup', - '"invalid json input"', - headers={'content-type': 'application/json'}, - status=400) - self.assertEqual(response.json['status'], 'error') - error_description = response.json['errors'][0]['description'] - self.assertIn('Should be a JSON object', error_description) + response = app.post( + "/signup", + '"invalid json input"', + headers={"content-type": "application/json"}, + status=400, + ) + self.assertEqual(response.json["status"], "error") + error_description = response.json["errors"][0]["description"] + self.assertIn("Should be a JSON object", error_description) def test_www_form_urlencoded(self): app = self.make_ordinary_app() - headers = {'content-type': 'application/x-www-form-urlencoded'} - response = app.post('/signup', - 'username=man', - headers=headers) - self.assertEqual(response.json['username'], 'man') + headers = {"content-type": "application/x-www-form-urlencoded"} + response = app.post("/signup", "username=man", headers=headers) + self.assertEqual(response.json["username"], "man") def test_multipart_form_data_one_field(self): app = self.make_ordinary_app() - response = app.post('/signup', - {'username': 'man'}, - content_type='multipart/form-data') - self.assertEqual(response.json['username'], 'man') + response = app.post("/signup", {"username": "man"}, content_type="multipart/form-data") + self.assertEqual(response.json["username"], "man") # Colander fails to parse multidict type return values # Therefore, test requires different schema with multiple keys, '/form' def test_multipart_form_data_multiple_fields(self): app = self.make_ordinary_app() - response = app.post('/form', - {'field1': 'man', 'field2': 'woman'}, - content_type='multipart/form-data') - self.assertEqual(response.json, {'field1': 'man', 'field2': 'woman'}) + response = app.post( + "/form", {"field1": "man", "field2": "woman"}, content_type="multipart/form-data" + ) + self.assertEqual(response.json, {"field1": "man", "field2": "woman"}) @skip_if_no_colander class TestBoundSchemas(LoggingCatcher, TestCase): - def make_ordinary_app(self): return TestApp(main({})) def test_bound_schema_existing_value(self): app = self.make_ordinary_app() - response = app.post_json('/bound', { - 'somefield': 'test', - }) - self.assertEqual(response.json['somefield'], 'test') + response = app.post_json( + "/bound", + { + "somefield": "test", + }, + ) + self.assertEqual(response.json["somefield"], "test") def test_bound_schema_non_existing_value(self): app = self.make_ordinary_app() - response = app.post_json('/bound', {}) - self.assertTrue(response.json['somefield'] > 0) + response = app.post_json("/bound", {}) + self.assertTrue(response.json["somefield"] > 0) def test_bound_schema_use_bound(self): app = self.make_ordinary_app() - response = app.post_json('/bound', {}, headers={'X-foo': '1'}) - self.assertEqual(response.json['somefield'], -10) + response = app.post_json("/bound", {}, headers={"X-foo": "1"}) + self.assertEqual(response.json["somefield"], -10) def test_bound_schema_multiple_calls(self): app = self.make_ordinary_app() - response = app.post_json('/bound', {}) - old = response.json['somefield'] - self.assertTrue(response.json['somefield'] > 0) - response = app.post_json('/bound', {}) - self.assertNotEqual(response.json['somefield'], old) + response = app.post_json("/bound", {}) + old = response.json["somefield"] + self.assertTrue(response.json["somefield"] > 0) + response = app.post_json("/bound", {}) + self.assertNotEqual(response.json["somefield"], old) @skip_if_no_colander class TestErrorMessageTranslationColander(TestCase): - def post(self, settings={}, headers={}): app = TestApp(main({}, **settings)) - return app.post_json('/foobar?yeah=test', { - 'foo': 'hello', - 'bar': 'open', - 'yeah': 'man', - 'ipsum': 10, - }, status=400, headers=headers) + return app.post_json( + "/foobar?yeah=test", + { + "foo": "hello", + "bar": "open", + "yeah": "man", + "ipsum": 10, + }, + status=400, + headers=headers, + ) def assertErrorDescription(self, response, message): - error_description = response.json['errors'][0]['description'] + error_description = response.json["errors"][0]["description"] self.assertEqual(error_description, message) def test_accept_language_header(self): response = self.post( - settings={'available_languages': 'fr en'}, - headers={'Accept-Language': 'fr'}) + settings={"available_languages": "fr en"}, headers={"Accept-Language": "fr"} + ) self.assertErrorDescription( - response, - u'10 est plus grand que la valeur maximum autorisΓ©e (3)') + response, "10 est plus grand que la valeur maximum autorisΓ©e (3)" + ) def test_default_language(self): - response = self.post(settings={ - 'available_languages': 'fr ja', - 'pyramid.default_locale_name': 'ja', - }) - self.assertErrorDescription( - response, - u'10 γ―ζœ€ε€§ε€€ 3 γ‚’θΆ…ιŽγ—γ¦γ„γΎγ™') + response = self.post( + settings={ + "available_languages": "fr ja", + "pyramid.default_locale_name": "ja", + } + ) + self.assertErrorDescription(response, "10 γ―ζœ€ε€§ε€€ 3 γ‚’θΆ…ιŽγ—γ¦γ„γΎγ™") def test_default_language_fallback(self): """Should fallback to default language if requested language is not available""" response = self.post( settings={ - 'available_languages': 'ja en', - 'pyramid.default_locale_name': 'ja', + "available_languages": "ja en", + "pyramid.default_locale_name": "ja", }, - headers={'Accept-Language': 'ru'}) - self.assertErrorDescription( - response, - u'10 γ―ζœ€ε€§ε€€ 3 γ‚’θΆ…ιŽγ—γ¦γ„γΎγ™') + headers={"Accept-Language": "ru"}, + ) + self.assertErrorDescription(response, "10 γ―ζœ€ε€§ε€€ 3 γ‚’θΆ…ιŽγ—γ¦γ„γΎγ™") def test_no_language_settings(self): response = self.post() - self.assertErrorDescription( - response, - u'10 is greater than maximum value 3') + self.assertErrorDescription(response, "10 is greater than maximum value 3") @skip_if_no_colander @@ -608,192 +587,184 @@ def test_no_body_schema(self): class TestExtractedJSONValueTypes(unittest.TestCase): """Make sure that all JSON string values extracted from the request - are unicode when running using PY2. + are unicode when running using PY2. """ + def test_extracted_json_values(self): """Extracted JSON values are unicode in PY2.""" body = b'{"foo": "bar", "currency": "\xe2\x82\xac"}' - request = Request.blank('/', body=body) + request = Request.blank("/", body=body) data = extract_cstruct(request) - self.assertEqual(type(data['body']['foo']), str) - self.assertEqual(type(data['body']['currency']), str) - self.assertEqual(data['body']['currency'], u'€') + self.assertEqual(type(data["body"]["foo"]), str) + self.assertEqual(type(data["body"]["currency"]), str) + self.assertEqual(data["body"]["currency"], "€") @skip_if_no_marshmallow class TestServiceDefinitionMarshmallow(LoggingCatcher, TestCase): - def test_multiple_querystrings(self): app = TestApp(main({})) # it is possible to have more than one value with the same name in the # querystring - self.assertEqual(b'{"field": ["5"]}', app.get('/m_foobaz?field=5').body) - self.assertEqual(b'{"field": ["5", "2"]}', - app.get('/m_foobaz?field=5&field=2').body) + self.assertEqual(b'{"field": ["5"]}', app.get("/m_foobaz?field=5").body) + self.assertEqual(b'{"field": ["5", "2"]}', app.get("/m_foobaz?field=5&field=2").body) def test_validated_body_content_from_schema(self): app = TestApp(main({})) - content = {'email': 'alexis@notmyidea.org'} - response = app.post_json('/newsletter', params=content) + content = {"email": "alexis@notmyidea.org"} + response = app.post_json("/newsletter", params=content) self.assertEqual(response.status_code, 200) - self.assertEqual(response.json['body'], content) + self.assertEqual(response.json["body"], content) def test_validated_querystring_content_from_schema(self): app = TestApp(main({})) - response = app.post_json('/m_newsletter?ref=3') + response = app.post_json("/m_newsletter?ref=3") self.assertEqual(response.status_code, 200) - self.assertEqual(response.json['querystring'], {"ref": 3}) + self.assertEqual(response.json["querystring"], {"ref": 3}) def test_validated_querystring_and_schema_from_same_schema(self): app = TestApp(main({})) - content = {'email': 'alexis@notmyidea.org'} - response = app.post_json('/m_newsletter?ref=20', params=content) + content = {"email": "alexis@notmyidea.org"} + response = app.post_json("/m_newsletter?ref=20", params=content) self.assertEqual(response.status_code, 200) - self.assertEqual(response.json['body'], content) - self.assertEqual(response.json['querystring'], {"ref": 20}) + self.assertEqual(response.json["body"], content) + self.assertEqual(response.json["querystring"], {"ref": 20}) - response = app.post_json('/m_newsletter?ref=2', params=content, - status=400) + response = app.post_json("/m_newsletter?ref=2", params=content, status=400) self.assertEqual(response.status_code, 400) - error = { - 'location': 'body', - 'name': 'email', - 'description': 'Invalid email length' - } - self.assertEqual(response.json['errors'][0], error) + error = {"location": "body", "name": "email", "description": "Invalid email length"} + self.assertEqual(response.json["errors"][0], error) def test_validated_path_content_from_schema(self): # Test validation request.matchdict. (See #411) app = TestApp(main({})) - response = app.get('/m_item/42', status=200) - self.assertEqual(response.json, {'item_id': 42}) + response = app.get("/m_item/42", status=200) + self.assertEqual(response.json, {"item_id": 42}) def test_content_type_with_no_body_should_pass(self): app = TestApp(main({})) - request = app.RequestClass.blank('/m_newsletter', method='POST', - headers={'Content-Type': - 'application/json'}) + request = app.RequestClass.blank( + "/m_newsletter", method="POST", headers={"Content-Type": "application/json"} + ) response = app.do_request(request, 200, True) self.assertEqual(response.status_code, 200) - self.assertEqual(response.json['body'], {}) + self.assertEqual(response.json["body"], {}) def test_content_type_missing_with_no_body_should_pass(self): app = TestApp(main({})) # requesting without a Content-Type header nor a body should # return a 200. - request = app.RequestClass.blank('/m_newsletter', method='POST') + request = app.RequestClass.blank("/m_newsletter", method="POST") response = app.do_request(request, 200, True) self.assertEqual(response.status_code, 200) - self.assertEqual(response.json['body'], {}) + self.assertEqual(response.json["body"], {}) def test_content_type_with_callable(self): # Test that using a callable for content_type works as well. app = TestApp(main({})) - response = app.post('/service6', headers={'Content-Type': 'audio/*'}, - status=415) - error_description = response.json['errors'][0]['description'] - self.assertIn('text/xml', error_description) - self.assertIn('application/json', error_description) + response = app.post("/service6", headers={"Content-Type": "audio/*"}, status=415) + error_description = response.json["errors"][0]["description"] + self.assertIn("text/xml", error_description) + self.assertIn("application/json", error_description) def test_content_type_with_callable_returning_scalar(self): # Test that using a callable for content_type works as well. # Now, the callable returns a scalar instead of a list. app = TestApp(main({})) - response = app.put('/service6', headers={'Content-Type': 'audio/*'}, - status=415) - error_description = response.json['errors'][0]['description'] - self.assertIn('text/xml', error_description) + response = app.put("/service6", headers={"Content-Type": "audio/*"}, status=415) + error_description = response.json["errors"][0]["description"] + self.assertIn("text/xml", error_description) def test_post(self, settings={}, headers={}): app = TestApp(main({}, **settings)) - response = app.post_json('/m_foobar?yeah=test', { - 'foo': 'hello', - 'bar': 'open', - 'yeah': 'man', - 'ipsum': 10, - }, status=400, headers=headers) + response = app.post_json( + "/m_foobar?yeah=test", + { + "foo": "hello", + "bar": "open", + "yeah": "man", + "ipsum": 10, + }, + status=400, + headers=headers, + ) - self.assertEqual(response.json['errors'][0]['location'], 'body') - self.assertEqual(response.json['errors'][0]['name'], 'ipsum') + self.assertEqual(response.json["errors"][0]["location"], "body") + self.assertEqual(response.json["errors"][0]["name"], "ipsum") @skip_if_no_marshmallow class TestRequestDataExtractorsMarshmallow(LoggingCatcher, TestCase): - def make_ordinary_app(self): return TestApp(main({})) def test_valid_json(self): app = self.make_ordinary_app() - response = app.post_json('/m_signup', { - 'username': 'man', - }) - self.assertEqual(response.json['username'], 'man') + response = app.post_json( + "/m_signup", + { + "username": "man", + }, + ) + self.assertEqual(response.json["username"], "man") def test_valid_nonstandard_json(self): app = self.make_ordinary_app() response = app.post_json( - '/m_signup', - {'username': 'man'}, - headers={'content-type': 'application/merge-patch+json'} + "/m_signup", + {"username": "man"}, + headers={"content-type": "application/merge-patch+json"}, ) - self.assertEqual(response.json['username'], 'man') + self.assertEqual(response.json["username"], "man") def test_valid_json_array(self): app = self.make_ordinary_app() - response = app.post_json( - '/m_group_signup', - [{'username': 'hey'}, {'username': 'how'}] - ) - self.assertEqual(response.json['data'], - [{'username': 'hey'}, {'username': 'how'}]) + response = app.post_json("/m_group_signup", [{"username": "hey"}, {"username": "how"}]) + self.assertEqual(response.json["data"], [{"username": "hey"}, {"username": "how"}]) def test_invalid_json(self): app = self.make_ordinary_app() - response = app.post('/m_signup', - '{"foo": "bar"', - headers={'content-type': 'application/json'}, - status=400) - self.assertEqual(response.json['status'], 'error') - error_description = response.json['errors'][0]['description'] - self.assertIn('Invalid JSON: Expecting', error_description) + response = app.post( + "/m_signup", '{"foo": "bar"', headers={"content-type": "application/json"}, status=400 + ) + self.assertEqual(response.json["status"], "error") + error_description = response.json["errors"][0]["description"] + self.assertIn("Invalid JSON: Expecting", error_description) def test_json_text(self): app = self.make_ordinary_app() - response = app.post('/m_signup', - '"invalid json input"', - headers={'content-type': 'application/json'}, - status=400) - self.assertEqual(response.json['status'], 'error') - error_description = response.json['errors'][0]['description'] - self.assertIn('Should be a JSON object', error_description) + response = app.post( + "/m_signup", + '"invalid json input"', + headers={"content-type": "application/json"}, + status=400, + ) + self.assertEqual(response.json["status"], "error") + error_description = response.json["errors"][0]["description"] + self.assertIn("Should be a JSON object", error_description) def test_www_form_urlencoded(self): app = self.make_ordinary_app() - headers = {'content-type': 'application/x-www-form-urlencoded'} - response = app.post('/m_signup', - 'username=man', - headers=headers) - self.assertEqual(response.json['username'], 'man') + headers = {"content-type": "application/x-www-form-urlencoded"} + response = app.post("/m_signup", "username=man", headers=headers) + self.assertEqual(response.json["username"], "man") def test_multipart_form_data_one_field(self): app = self.make_ordinary_app() - response = app.post('/m_signup', - {'username': 'man'}, - content_type='multipart/form-data') - self.assertEqual(response.json['username'], 'man') + response = app.post("/m_signup", {"username": "man"}, content_type="multipart/form-data") + self.assertEqual(response.json["username"], "man") # Marshmallow fails to parse multidict type return values # Therefore, test requires different schema with multiple keys, '/m_form' def test_multipart_form_data_multiple_fields(self): app = self.make_ordinary_app() - response = app.post('/m_form', - {'field1': 'man', 'field2': 'woman'}, - content_type='multipart/form-data') - self.assertEqual(response.json, {'field1': 'man', 'field2': 'woman'}) + response = app.post( + "/m_form", {"field1": "man", "field2": "woman"}, content_type="multipart/form-data" + ) + self.assertEqual(response.json, {"field1": "man", "field2": "woman"}) @skip_if_no_marshmallow @@ -813,57 +784,49 @@ def test_no_body_schema(self): self.assertEqual(len(request.errors), 0) def test_message_normalizer_no_field_names(self): - from marshmallow.exceptions import ValidationError from cornice.validators._marshmallow import _message_normalizer - parsed = _message_normalizer(ValidationError('Test message')) - self.assertEqual({'_schema': ['Test message']}, parsed) + from marshmallow.exceptions import ValidationError + + parsed = _message_normalizer(ValidationError("Test message")) + self.assertEqual({"_schema": ["Test message"]}, parsed) def test_message_normalizer_field_names(self): - from marshmallow.exceptions import ValidationError from cornice.validators._marshmallow import _message_normalizer + from marshmallow.exceptions import ValidationError - parsed = _message_normalizer( - ValidationError('Test message', field_names=['test']) - ) - self.assertEqual({'test': ['Test message']}, parsed) + parsed = _message_normalizer(ValidationError("Test message", field_names=["test"])) + self.assertEqual({"test": ["Test message"]}, parsed) def test_instantiated_schema(self): app = TestApp(main({})) with self.assertRaises(ValueError): - app.post('/m_item/42', status=200) + app.post("/m_item/42", status=200) @skip_if_no_marshmallow class TestContextSchemas(LoggingCatcher, TestCase): - def make_ordinary_app(self): return TestApp(main({})) def test_schema_existing_value(self): app = self.make_ordinary_app() - response = app.post_json('/m_bound', { - 'somefield': 99, - 'csrf_secret': 'secret' - }) - self.assertEqual(response.json['somefield'], 99) + response = app.post_json("/m_bound", {"somefield": 99, "csrf_secret": "secret"}) + self.assertEqual(response.json["somefield"], 99) def test_schema_wrong_token(self): app = self.make_ordinary_app() - response = app.post_json('/m_bound', {}, status=400) - self.assertEqual( - response.json['errors'][0]['description'][0], - "Wrong token" - ) + response = app.post_json("/m_bound", {}, status=400) + self.assertEqual(response.json["errors"][0]["description"][0], "Wrong token") def test_schema_non_existing_value(self): app = self.make_ordinary_app() - response = app.post_json('/m_bound', {'csrf_secret': 'secret'}) - self.assertTrue(response.json['somefield'] > 0) + response = app.post_json("/m_bound", {"csrf_secret": "secret"}) + self.assertTrue(response.json["somefield"] > 0) def test_schema_multiple_calls(self): app = self.make_ordinary_app() - response = app.post_json('/m_bound', {'csrf_secret': 'secret'}) - old = response.json['somefield'] - self.assertTrue(response.json['somefield'] > 0) - response = app.post_json('/bound', {'csrf_secret': 'secret'}) - self.assertNotEqual(response.json['somefield'], old) + response = app.post_json("/m_bound", {"csrf_secret": "secret"}) + old = response.json["somefield"] + self.assertTrue(response.json["somefield"] > 0) + response = app.post_json("/bound", {"csrf_secret": "secret"}) + self.assertNotEqual(response.json["somefield"], old) diff --git a/tests/validationapp.py b/tests/validationapp.py index 003605a2..94d24b18 100644 --- a/tests/validationapp.py +++ b/tests/validationapp.py @@ -3,11 +3,10 @@ # You can obtain one at http://mozilla.org/MPL/2.0/. import json +from cornice import Service from pyramid.config import Configurator from pyramid.httpexceptions import HTTPBadRequest -from cornice import Service - from .support import CatchErrors @@ -16,24 +15,24 @@ def has_payed(request, **kw): - if 'paid' not in request.GET: - request.errors.add('body', 'paid', 'You must pay!') + if "paid" not in request.GET: + request.errors.add("body", "paid", "You must pay!") def foo_int(request, **kw): - if 'foo' not in request.GET: + if "foo" not in request.GET: return try: - request.validated['foo'] = int(request.GET['foo']) + request.validated["foo"] = int(request.GET["foo"]) except ValueError: - request.errors.add('url', 'foo', 'Not an int') + request.errors.add("url", "foo", "Not an int") @service.get(validators=(has_payed, foo_int)) def get1(request): res = {"test": "succeeded"} try: - res['foo'] = request.validated['foo'] + res["foo"] = request.validated["foo"] except KeyError: pass @@ -43,9 +42,9 @@ def get1(request): def _json(request, **kw): """The request body should be a JSON object.""" try: - request.validated['json'] = json.loads(request.body.decode('utf-8')) + request.validated["json"] = json.loads(request.body.decode("utf-8")) except ValueError: - request.errors.add('body', 'json', 'Not a json body') + request.errors.add("body", "json", "Not a json body") @service.post(validators=_json) @@ -67,8 +66,8 @@ def get2(request): service3 = Service(name="service3", path="/service3") -@service3.get(accept=lambda request: ('application/json', 'text/plain')) -@service3.put(accept=lambda request: ('application/json', 'text/plain')) +@service3.get(accept=lambda request: ("application/json", "text/plain")) +@service3.put(accept=lambda request: ("application/json", "text/plain")) def get3(request): return {"body": "yay!"} @@ -77,30 +76,34 @@ def _filter(response): response.body = b"filtered response" return response + service4 = Service(name="service4", path="/service4") def fail(request, **kw): - request.errors.add('body', 'xml', 'Not XML') + request.errors.add("body", "xml", "Not XML") def xml_error(request): errors = request.errors - lines = [''] + lines = [""] for error in errors: - lines.append('' - '%(location)s' - '%(name)s' - '%(description)s' - '' % error) - lines.append('') - return HTTPBadRequest(body=''.join(lines)) + lines.append( + "" + "%(location)s" + "%(name)s" + "%(description)s" + "" % error + ) + lines.append("") + return HTTPBadRequest(body="".join(lines)) @service4.post(validators=fail, error_handler=xml_error) def post4(request): raise ValueError("Shouldn't get here") + # test filtered services filtered_service = Service(name="filtered", path="/filtered", filters=_filter) @@ -116,8 +119,8 @@ def get4(request): @service5.get() -@service5.post(content_type='application/json') -@service5.put(content_type=('text/plain', 'application/json')) +@service5.post(content_type="application/json") +@service5.put(content_type=("text/plain", "application/json")) def post5(request): return "some response" @@ -126,8 +129,8 @@ def post5(request): service6 = Service(name="service6", path="/service6") -@service6.post(content_type=lambda request: ('text/xml', 'application/json')) -@service6.put(content_type=lambda request: 'text/xml') +@service6.post(content_type=lambda request: ("text/xml", "application/json")) +@service6.put(content_type=lambda request: "text/xml") def post6(request): return {"body": "yay!"} @@ -136,36 +139,33 @@ def post6(request): service7 = Service(name="service7", path="/service7") -@service7.post(accept='text/xml', content_type='application/json') -@service7.put(accept=('text/xml', 'text/plain'), - content_type=('application/json', 'text/xml')) +@service7.post(accept="text/xml", content_type="application/json") +@service7.put(accept=("text/xml", "text/plain"), content_type=("application/json", "text/xml")) def post7(request): return "some response" try: from colander import ( + Email, + Integer, Invalid, MappingSchema, - SequenceSchema, + Range, SchemaNode, + SequenceSchema, String, - Integer, - Range, - Email, + deferred, drop, null, - deferred ) - - from cornice.validators import colander_validator, colander_body_validator + from cornice.validators import colander_body_validator, colander_validator COLANDER = True except ImportError: COLANDER = False if COLANDER: - # services for colander validation signup = Service(name="signup", path="/signup") bound = Service(name="bound", path="/bound") @@ -174,32 +174,35 @@ def post7(request): body_signup = Service(name="body signup", path="/body_signup") foobar = Service(name="foobar", path="/foobar") foobaz = Service(name="foobaz", path="/foobaz") - email_service = Service(name='newsletter', path='/newsletter') - item_service = Service(name='item', path='/item/{item_id}') + email_service = Service(name="newsletter", path="/newsletter") + item_service = Service(name="item", path="/item/{item_id}") form_service = Service(name="form", path="/form") - class SignupSchema(MappingSchema): username = SchemaNode(String()) @deferred def deferred_missing(node, kw): import random - return kw.get('missing_foo') or random.random() + + return kw.get("missing_foo") or random.random() class NeedsBindingSchema(MappingSchema): somefield = SchemaNode(String(), missing=deferred_missing) def rebinding_validator(request, **kwargs): - kwargs['schema'] = NeedsBindingSchema().bind() + kwargs["schema"] = NeedsBindingSchema().bind() return colander_body_validator(request, **kwargs) @bound.post(schema=NeedsBindingSchema().bind(), validators=(rebinding_validator,)) def bound_post(request): return request.validated - @bound.post(schema=NeedsBindingSchema().bind(missing_foo=-10), - validators=(colander_body_validator,), header='X-foo') + @bound.post( + schema=NeedsBindingSchema().bind(missing_foo=-10), + validators=(colander_body_validator,), + header="X-foo", + ) def bound_post_with_override(request): return request.validated @@ -207,64 +210,56 @@ def bound_post_with_override(request): def signup_post(request): return request.validated - class GroupSignupSchema(SequenceSchema): user = SignupSchema() - @group_signup.post(schema=GroupSignupSchema(), - validators=(colander_body_validator,)) + @group_signup.post(schema=GroupSignupSchema(), validators=(colander_body_validator,)) def group_signup_post(request): - return {'data': request.validated} + return {"data": request.validated} class BodyGroupSignupSchema(MappingSchema): body = GroupSignupSchema() - @body_group_signup.post(schema=BodyGroupSignupSchema(), - validators=(colander_validator,)) + @body_group_signup.post(schema=BodyGroupSignupSchema(), validators=(colander_validator,)) def body_group_signup_post(request): - return {'data': request.validated['body']} - + return {"data": request.validated["body"]} class BodySignupSchema(MappingSchema): body = SignupSchema() - @body_signup.post(schema=BodySignupSchema(), - validators=(colander_body_validator,)) + @body_signup.post(schema=BodySignupSchema(), validators=(colander_body_validator,)) def body_signup_post(request): - return {'data': request.validated} - + return {"data": request.validated} def validate_bar(node, value, **kwargs): - if value != 'open': + if value != "open": raise Invalid(node, "The bar is not open.") class Integers(SequenceSchema): - integer = SchemaNode(Integer(), type='int') + integer = SchemaNode(Integer(), type="int") class BodySchema(MappingSchema): # foo and bar are required, baz is optional foo = SchemaNode(String()) bar = SchemaNode(String(), validator=validate_bar) baz = SchemaNode(String(), missing=None) - ipsum = SchemaNode(Integer(), missing=1, - validator=Range(0, 3)) + ipsum = SchemaNode(Integer(), missing=1, validator=Range(0, 3)) integers = Integers(missing=()) class Query(MappingSchema): - yeah = SchemaNode(String(), type='str') + yeah = SchemaNode(String(), type="str") class RequestSchema(MappingSchema): body = BodySchema() querystring = Query() def deserialize(self, cstruct): - if 'body' in cstruct and cstruct['body'] == b'hello,open,yeah': - values = cstruct['body'].decode().split(',') - cstruct['body'] = dict(zip(['foo', 'bar', 'yeah'], values)) + if "body" in cstruct and cstruct["body"] == b"hello,open,yeah": + values = cstruct["body"].decode().split(",") + cstruct["body"] = dict(zip(["foo", "bar", "yeah"], values)) return MappingSchema.deserialize(self, cstruct) - @foobar.post(schema=RequestSchema(), validators=(colander_validator,)) def foobar_post(request): return {"test": "succeeded"} @@ -276,17 +271,16 @@ class ListQuerystringSequence(MappingSchema): field = StringSequence() def deserialize(self, cstruct): - if 'field' in cstruct and not isinstance(cstruct['field'], list): - cstruct['field'] = [cstruct['field']] + if "field" in cstruct and not isinstance(cstruct["field"], list): + cstruct["field"] = [cstruct["field"]] return MappingSchema.deserialize(self, cstruct) class QSSchema(MappingSchema): querystring = ListQuerystringSequence() - @foobaz.get(schema=QSSchema(), validators=(colander_validator,)) def foobaz_get(request): - return {"field": request.validated['querystring']['field']} + return {"field": request.validated["querystring"]["field"]} class NewsletterSchema(MappingSchema): email = SchemaNode(String(), validator=Email(), missing=drop) @@ -300,18 +294,16 @@ class NewsletterPayload(MappingSchema): def deserialize(self, cstruct=null): appstruct = super(NewsletterPayload, self).deserialize(cstruct) - email = appstruct['body'].get('email') - ref = appstruct['querystring'].get('ref') + email = appstruct["body"].get("email") + ref = appstruct["querystring"].get("ref") if email and ref and len(email) != ref: body_node, _ = self.children exc = Invalid(body_node) - exc["email"] = 'Invalid email length' + exc["email"] = "Invalid email length" raise exc return appstruct - - @email_service.post(schema=NewsletterPayload(), - validators=(colander_validator,)) + @email_service.post(schema=NewsletterPayload(), validators=(colander_validator,)) def newsletter(request): return request.validated @@ -321,32 +313,27 @@ class ItemPathSchema(MappingSchema): class ItemSchema(MappingSchema): path = ItemPathSchema() - - @item_service.get(schema=ItemSchema(), - validators=(colander_validator,)) + @item_service.get(schema=ItemSchema(), validators=(colander_validator,)) def item(request): - return request.validated['path'] + return request.validated["path"] class FormSchema(MappingSchema): field1 = SchemaNode(String()) field2 = SchemaNode(String()) - - @form_service.post(schema=FormSchema(), - validators=(colander_body_validator,)) + @form_service.post(schema=FormSchema(), validators=(colander_body_validator,)) def form(request): return request.validated + try: import marshmallow + try: from marshmallow.utils import EXCLUDE except ImportError: - EXCLUDE = 'exclude' - from cornice.validators import ( - marshmallow_validator, - marshmallow_body_validator - ) + EXCLUDE = "exclude" + from cornice.validators import marshmallow_body_validator, marshmallow_validator MARSHMALLOW = True except ImportError: @@ -360,15 +347,15 @@ def form(request): m_group_signup = Service(name="m_group signup", path="/m_group_signup") m_foobar = Service(name="m_foobar", path="/m_foobar") m_foobaz = Service(name="m_foobaz", path="/m_foobaz") - m_email_service = Service(name='m_newsletter', path='/m_newsletter') - m_item_service = Service(name='m_item', path='/m_item/{item_id}') + m_email_service = Service(name="m_newsletter", path="/m_newsletter") + m_item_service = Service(name="m_item", path="/m_item/{item_id}") m_form_service = Service(name="m_form", path="/m_form") - class MSignupSchema(marshmallow.Schema): class Meta: strict = True unknown = EXCLUDE + username = marshmallow.fields.String() import random @@ -377,22 +364,21 @@ class MNeedsContextSchema(marshmallow.Schema): class Meta: strict = True unknown = EXCLUDE + somefield = marshmallow.fields.Float(missing=lambda: random.random()) csrf_secret = marshmallow.fields.String() @marshmallow.validates_schema def validate_csrf_secret(self, data, **kwargs): # simulate validation of session variables - if self.context['request'].get_csrf() != data.get('csrf_secret'): - raise marshmallow.ValidationError('Wrong token') + if self.context["request"].get_csrf() != data.get("csrf_secret"): + raise marshmallow.ValidationError("Wrong token") - @m_bound.post(schema=MNeedsContextSchema, - validators=(marshmallow_body_validator,)) + @m_bound.post(schema=MNeedsContextSchema, validators=(marshmallow_body_validator,)) def m_bound_post(request): return request.validated - @m_signup.post( - schema=MSignupSchema, validators=(marshmallow_body_validator,)) + @m_signup.post(schema=MSignupSchema, validators=(marshmallow_body_validator,)) def signup_post(request): return request.validated @@ -400,40 +386,42 @@ def signup_post(request): # schema initialisation. In our case it passes many=True to the desired # schema def get_my_marshmallow_validator_with_kwargs(request, **kwargs): - kwargs['schema'] = MSignupSchema - kwargs['schema_kwargs'] = {'many': True} + kwargs["schema"] = MSignupSchema + kwargs["schema_kwargs"] = {"many": True} return marshmallow_body_validator(request, **kwargs) @m_group_signup.post(validators=(get_my_marshmallow_validator_with_kwargs,)) def m_group_signup_post(request): - return {'data': request.validated} + return {"data": request.validated} def m_validate_bar(node, value): - if value != 'open': + if value != "open": raise Invalid(node, "The bar is not open.") class MBodySchema(marshmallow.Schema): class Meta: strict = True unknown = EXCLUDE + # foo and bar are required, baz is optional foo = marshmallow.fields.String() bar = SchemaNode(String(), validator=m_validate_bar) baz = marshmallow.fields.String(missing=None) - ipsum = marshmallow.fields.Integer( - missing=1, validate=marshmallow.validate.Range(0,3)) + ipsum = marshmallow.fields.Integer(missing=1, validate=marshmallow.validate.Range(0, 3)) integers = marshmallow.fields.List(marshmallow.fields.Integer()) class MQuery(marshmallow.Schema): class Meta: strict = True unknown = EXCLUDE + yeah = marshmallow.fields.String() class MRequestSchema(marshmallow.Schema): class Meta: strict = True unknown = EXCLUDE + body = marshmallow.fields.Nested(MBodySchema) querystring = marshmallow.fields.Nested(MQuery) @@ -441,14 +429,13 @@ class Meta: def m_foobar_post(request): return {"test": "succeeded"} - class MListQuerystringSequenced(marshmallow.Schema): field = marshmallow.fields.List(marshmallow.fields.String(), many=True) @marshmallow.pre_load() def normalize_field(self, data, **kwargs): - if 'field' in data and not isinstance(data['field'], list): - data['field'] = [data['field']] + if "field" in data and not isinstance(data["field"], list): + data["field"] = [data["field"]] return data class MQSSchema(marshmallow.Schema): @@ -458,21 +445,22 @@ class Meta: querystring = marshmallow.fields.Nested(MListQuerystringSequenced) - @m_foobaz.get(schema=MQSSchema, validators=(marshmallow_validator,)) def m_foobaz_get(request): - return {"field": request.validated['querystring']['field']} + return {"field": request.validated["querystring"]["field"]} class MNewsletterSchema(marshmallow.Schema): class Meta: strict = True unknown = EXCLUDE + email = marshmallow.fields.String(validate=marshmallow.validate.Email()) class MRefererSchema(marshmallow.Schema): class Meta: strict = True unknown = EXCLUDE + ref = marshmallow.fields.Integer() class MNewsletterPayload(marshmallow.Schema): @@ -484,14 +472,12 @@ class Meta: @marshmallow.validates_schema def validate_email_length(self, data, **kwargs): - email = data['body'].get('email') - ref = data['querystring'].get('ref') + email = data["body"].get("email") + ref = data["querystring"].get("ref") if email and ref and len(email) != ref: - raise marshmallow.ValidationError( - {'body': {'email': 'Invalid email length'}}) + raise marshmallow.ValidationError({"body": {"email": "Invalid email length"}}) - @m_email_service.post( - schema=MNewsletterPayload, validators=(marshmallow_validator,)) + @m_email_service.post(schema=MNewsletterPayload, validators=(marshmallow_validator,)) def m_newsletter(request): return request.validated @@ -499,35 +485,33 @@ class MItemPathSchema(marshmallow.Schema): class Meta: strict = True unknown = EXCLUDE + item_id = marshmallow.fields.Integer(missing=None) class MItemSchema(marshmallow.Schema): class Meta: strict = True unknown = EXCLUDE - path = marshmallow.fields.Nested(MItemPathSchema) + path = marshmallow.fields.Nested(MItemPathSchema) - @m_item_service.get( - schema=MItemSchema, validators=(marshmallow_validator,)) + @m_item_service.get(schema=MItemSchema, validators=(marshmallow_validator,)) def m_item(request): - return request.validated['path'] + return request.validated["path"] - @m_item_service.post( - schema=MItemSchema(), validators=(marshmallow_validator,)) + @m_item_service.post(schema=MItemSchema(), validators=(marshmallow_validator,)) def m_item_fails(request): - return request.validated['path'] + return request.validated["path"] class MFormSchema(marshmallow.Schema): class Meta: strict = True unknown = EXCLUDE + field1 = marshmallow.fields.String() field2 = marshmallow.fields.String() - - @m_form_service.post( - schema=MFormSchema, validators=(marshmallow_body_validator,)) + @m_form_service.post(schema=MFormSchema, validators=(marshmallow_body_validator,)) def m_form(request): return request.validated @@ -541,5 +525,5 @@ def main(global_config, **settings): config = Configurator(settings=settings) config.include(includeme) # used for simulating pyramid session object access in validators - config.add_request_method(lambda x: 'secret', 'get_csrf') + config.add_request_method(lambda x: "secret", "get_csrf") return CatchErrors(config.make_wsgi_app()) From 9f60c998cde8bd20ba319c7917c22ab63d487367 Mon Sep 17 00:00:00 2001 From: Mathieu Leplatre Date: Tue, 30 Jan 2024 19:48:17 +0100 Subject: [PATCH 3/9] Rename from master to main --- README.rst | 6 +++--- docs/source/tutorial.rst | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 11b9e095..f445abb3 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ Cornice ======= -|readthedocs| |pypi| |github-actions| |master-coverage| +|readthedocs| |pypi| |github-actions| |main-coverage| .. |github-actions| image:: https://github.com/Cornices/cornice/workflows/Unit%20Testing/badge.svg :target: https://github.com/Cornices/cornice/actions?query=workflow%3A%22Unit+Testing%22 @@ -11,8 +11,8 @@ Cornice :target: https://cornice.readthedocs.io/en/latest/ :alt: Documentation Status -.. |master-coverage| image:: - https://coveralls.io/repos/Cornices/cornice/badge.svg?branch=master +.. |main-coverage| image:: + https://coveralls.io/repos/Cornices/cornice/badge.svg?branch=main :alt: Coverage :target: https://coveralls.io/r/Cornices/cornice diff --git a/docs/source/tutorial.rst b/docs/source/tutorial.rst index 3444b763..17c88af3 100755 --- a/docs/source/tutorial.rst +++ b/docs/source/tutorial.rst @@ -6,7 +6,7 @@ Full tutorial Let's create a full working application with **Cornice**. We want to create a light messaging service. -You can find its whole source code at https://github.com/Cornices/examples/blob/master/messaging +You can find its whole source code at https://github.com/Cornices/examples/blob/main/messaging Features: @@ -307,4 +307,4 @@ A simple client to use against our service can do three things: Without going into great details, there's a Python CLI against messaging that uses Curses. -See https://github.com/Cornices/examples/blob/master/messaging/messaging/client.py +See https://github.com/Cornices/examples/blob/main/messaging/messaging/client.py From a1acbd0580f075af935f19107871bd0f7764f037 Mon Sep 17 00:00:00 2001 From: Mathieu Leplatre Date: Tue, 30 Jan 2024 19:51:51 +0100 Subject: [PATCH 4/9] Build docs in CI --- .github/workflows/test.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 44f87d61..46e33271 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,3 +36,14 @@ jobs: - name: Coveralls uses: coverallsapp/github-action@v2 + + docs: + runs-on: ubuntu-latest + needs: lint + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-python@v4 + + - name: Build docs + run: make docs \ No newline at end of file From 273ce3240bd6cc0beca747a27999d202b5d0a4b9 Mon Sep 17 00:00:00 2001 From: Mathieu Leplatre Date: Tue, 30 Jan 2024 19:53:31 +0100 Subject: [PATCH 5/9] Add contributors docs --- .github/CONTRIBUTING.md | 52 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .github/CONTRIBUTING.md diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 00000000..2d9f72af --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,52 @@ +How to contribute +================= + +Thanks for your interest in contributing! + +## Reporting Bugs + +Report bugs at https://github.com/Cornices/cornice/issues/new + +If you are reporting a bug, please include: + + - Any details about your local setup that might be helpful in troubleshooting. + - Detailed steps to reproduce the bug or even a PR with a failing tests if you can. + + +## Ready to contribute? + +### Getting Started + + - Fork the repo on GitHub and clone locally: + +```bash +git clone git@github.com:Cornices/cornice.git +git remote add {your_name} git@github.com:{your_name}/cornice.git +``` + +## Testing + + - `make test` to run all the tests + +## Submitting Changes + +```bash +git checkout main +git pull origin main +git checkout -b issue_number-bug-title +git commit # Your changes +git push -u {your_name} issue_number-bug-title +``` + +Then you can create a Pull-Request. +Please create your pull-request as soon as you have at least one commit even if it has only failing tests. This will allow us to help and give guidance. + +You will be able to update your pull-request by pushing commits to your branch. + + +## Releasing + +1. Create a release on Github on https://github.com/Cornices/cornice/releases/new +2. Create a new tag `X.Y.Z` (*This tag will be created from the target when you publish this release.*) +3. Generate release notes +4. Publish release From e5061c594c860a49422a29f29b256acdeedef9f2 Mon Sep 17 00:00:00 2001 From: Mathieu Leplatre Date: Tue, 30 Jan 2024 20:00:43 +0100 Subject: [PATCH 6/9] Add sphinx to docs requirements --- docs/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/requirements.txt b/docs/requirements.txt index c401ad1f..b463e90e 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,4 @@ mozilla-sphinx-theme==0.2 cornice_sphinx colander +Sphinx From e5f24e329380f97e70c99c33f41f46d01fd5c808 Mon Sep 17 00:00:00 2001 From: Mathieu Leplatre Date: Tue, 30 Jan 2024 20:21:02 +0100 Subject: [PATCH 7/9] Fix sphinx-build command from make --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index cfee355a..74b3f4fa 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ VENV := $(shell echo $${VIRTUAL_ENV-.venv}) PYTHON = $(VENV)/bin/python +SPHINX_BUILD = $(shell realpath ${VENV})/bin/sphinx-build INSTALL_STAMP = $(VENV)/.install.stamp .PHONY: all @@ -34,7 +35,7 @@ format: install $(VENV)/bin/ruff format src tests docs: install - cd docs && $(MAKE) html SPHINXBUILD=$(VENV)/bin/sphinx-build + cd docs && $(MAKE) html SPHINXBUILD=$(SPHINX_BUILD) .IGNORE: clean clean: From ace303c577d5e21fdd0bbf7de136309e900f599a Mon Sep 17 00:00:00 2001 From: Mathieu Leplatre Date: Wed, 31 Jan 2024 12:28:42 +0100 Subject: [PATCH 8/9] Pin dev dependencies with version > 1 --- pyproject.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6287d95a..a91f79ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,12 +34,12 @@ build-backend = "setuptools.build_meta" [project.optional-dependencies] dev = [ "ruff", - "pytest", - "pytest-cache", - "pytest-cov", - "webtest", + "pytest<9", + "pytest-cache<2", + "pytest-cov<5", + "WebTest<4", "marshmallow<4", - "colander", + "colander<3", ] [tool.pip-tools] From ac4f24c1432a6ba90e3af581e16f7e1804047931 Mon Sep 17 00:00:00 2001 From: Mathieu Leplatre Date: Wed, 31 Jan 2024 12:28:56 +0100 Subject: [PATCH 9/9] Add missing files --- .github/CODEOWNERS | 1 + .github/SECURITY.md | 12 ++++++++++++ 2 files changed, 13 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .github/SECURITY.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..d5c214f1 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @Cornices/cornice diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 00000000..6a434639 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,12 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| 6.x.x | :white_check_mark: | +| < 6.0 | :x: | + +## Reporting a Vulnerability + +If you believe you have found a Cornice-related security vulnerability, please [report it in a security advisory](https://github.com/Cornices/cornice/security/advisories/new).