diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 1b08e77b..ec28c2d1 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.10.0 +current_version = 1.10.2 commit = True tag = True diff --git a/.gitignore b/.gitignore index f5a8e6ae..7862503e 100644 --- a/.gitignore +++ b/.gitignore @@ -78,4 +78,5 @@ man/ .pytest_cache #vscode -.vscode/ \ No newline at end of file +.vscode/ +pip-wheel-metadata diff --git a/.travis.yml b/.travis.yml index 8701b1d4..1daedc65 100644 --- a/.travis.yml +++ b/.travis.yml @@ -49,4 +49,5 @@ jobs: password: secure: GoFaAo3a08PoPklGQ68q7EMYE59hb1Z1+PEMuSSXKelL2amyG6/kq5t7r8p6trjFabco1u94IwZFFlmUdfr4PqDQ/eaM9MVF1VvI8dxp/fg3WgSxjtxy4io/ELUWTGXTGs3V8Lp61haRdHjrqk5DwG6SHDVVzjuJBZ4Kydr6fuo0RWu9C4qAkU1MWmoIPeyeB4ZydXimD15uOvMp13UA7SqLUdXlAhwyL7c2h6pihu2gnbLXEnfkh8rlvRT8fSp7PUkFsg3FOvgMZg8dn8/J0I/pTmG7c71v0hacOVTKLsfAtSSHAjgcc8KffvMBOcE/YXgUr+lgR4R181lMqXv2LBNMuE5Gyc8GBjTb8Liiowpvg8XXH86vC/k+0/3toKWPobjsXJCAgR21+B7FFzE8hmZdP43HcSfCqbzS8ELUWF9AvUBlFtdakr0a0wLjcKsJyBNiKwwmw50kKsb7agpIXgS/I0Y6EyGwyXi8/q7IPmR7wffg3bAGnvvg5yz7vOZTGYJnFM+Ky1LdQ7fJ44D+Ze/SraB1syK2pmednX6YVMgIlPU5QpaprmQ9AkeoWmPZBShWl07Js73BnbQKWklkV8yAXq9qy51cjRA9SWQIFzeiHScOwhUalb1CpggOyG1sx4/GMFLg5nNE0ldKDfqUGJ4kbcNh25jiiBEJ7mEQkB0= on: + branch: master tags: true diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 17282b36..2e502c33 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,35 @@ Changelog ========= +v1.10.2 (2019-09-19) +-------------------- + +Fix +~~~ +- Update authenticators to catch Forbidden exception (#133) [Marc-Éric] + + +v1.10.1 (2019-09-19) +-------------------- + +Changes +~~~~~~~ +- Tweaking build rules, updating docs, and prepping for bumpversion do- + over. [Rick Riensche] + +Fix +~~~ +- Treat "description" key the same way as "explode" key for query and h… + (#129) [Artem Revenko] + +Other +~~~~~ +- Accept bare class for schema arguments (#126) [Rick Riensche] +- Fix marshmallow test helpers so that they work will all unittest + compatible frameworks and not just pytest. 'python setup.py test' + works again. (#127) [Andrew Standley] + + v1.10.0 (2019-09-11) -------------------- - BP-763: Add support for multiple authenticators (#122) [Andrew diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 1596efc8..1304853e 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -67,22 +67,30 @@ Pull Requests Releasing to PyPI ----------------- -Travis CI handles releasing package versions to PyPI. +We use Travis CI to automate releasing package versions to PyPI. + +.. warning:: These steps must be completed by an administrator. We generally do at least patch releases fairly frequently, but if you have a feature that urgently requires release, feel free to reach out and request one and we'll do our best to accommodate. + Flask-Rebar uses `semantic versions `_. Once you know the appropriate version part to bump, use the ``bumpversion`` tool which will bump the package version, add a commit, and tag the commit appropriately. Note, it's not a bad idea to do a manual inspection and any cleanup you deem necessary after running ``gitchangelog`` to ensure it looks good before then committing a "@cosmetic" update. +.. note:: Before completing the following steps, you will need to temporarily change settings on GitHub under branch protection rules to NOT include administrators. This is required to allow you to push the changelog update. + .. code-block:: bash - git checkout -b your-release-branch - bumpversion minor + git checkout master + git pull # just to play it safe and make sure you're up to date + bumpversion patch # or major or minor if applicable gitchangelog + # STOP HERE: inspect CHANGELOG.rst and clean up as needed before continuing git commit -a -m "@cosmetic - changelog" - -Then push the new commit and tags: +Then push the new commits and tags: .. code-block:: bash - git push -u origin your-release-branch --tags + git push && git push --tags + +Finally, while you're waiting for Travis CI to pick up the tagged version, build it, and deploy it to PyPi, don't forget to reset branch protection settings (for normal operation, administrators should be subject to these restrictions to enforce PR code review requirements). + -Create a Pull Request and merge back into master. Voila. diff --git a/flask_rebar/rebar.py b/flask_rebar/rebar.py index fc5345b4..becf9594 100644 --- a/flask_rebar/rebar.py +++ b/flask_rebar/rebar.py @@ -32,10 +32,16 @@ from flask_rebar.utils.request_utils import get_header_params_or_400 from flask_rebar.utils.request_utils import get_json_body_params_or_400 from flask_rebar.utils.request_utils import get_query_string_params_or_400 +from flask_rebar.utils.request_utils import normalize_schema from flask_rebar.utils.deprecation import deprecated, deprecated_parameters from flask_rebar.swagger_generation import SwaggerV2Generator from flask_rebar.swagger_ui import create_swagger_ui_blueprint +# Deal with maintaining (for now at least) support for 2.7+: +try: + from collections.abc import Mapping # 3.3+ +except ImportError: + from collections import Mapping # 2.7+ # To catch redirection exceptions, app.errorhandler expects 301 in versions # below 0.11.0 but the exception itself in versions greater than 0.11.0. @@ -119,7 +125,7 @@ def wrapped(*args, **kwargs): try: authenticator.authenticate() break # Short-circuit on first successful authentication - except errors.Unauthorized as e: + except (errors.Unauthorized, errors.Forbidden) as e: first_error = first_error or e else: raise first_error or errors.Unauthorized @@ -459,8 +465,19 @@ def add_handler( :param Type[USE_DEFAULT]|None|str mimetype: Content-Type header to add to the response schema """ - if isinstance(response_body_schema, marshmallow.Schema): - response_body_schema = {200: response_body_schema} + # Fix #115: if we were passed bare classes we'll go ahead and instantiate + headers_schema = normalize_schema(headers_schema) + request_body_schema = normalize_schema(request_body_schema) + query_string_schema = normalize_schema(query_string_schema) + if response_body_schema: + # Ensure we wrap in appropriate default (200) dict if we were passed a single Schema or class: + if not isinstance(response_body_schema, Mapping): + response_body_schema = {200: response_body_schema} + # use normalize_schema to convert any class reference(s) to instantiated schema(s): + response_body_schema = { + code: normalize_schema(schema) + for (code, schema) in response_body_schema.items() + } # authenticators can be a list of Authenticators, a single Authenticator, USE_DEFAULT, or None if isinstance(authenticators, Authenticator) or authenticators is USE_DEFAULT: diff --git a/flask_rebar/swagger_generation/swagger_generator_v3.py b/flask_rebar/swagger_generation/swagger_generator_v3.py index ce5c424a..d3720b3b 100644 --- a/flask_rebar/swagger_generation/swagger_generator_v3.py +++ b/flask_rebar/swagger_generation/swagger_generator_v3.py @@ -324,6 +324,7 @@ def _convert_schema_to_list_of_parameters(self, schema, converter, in_): # Pardon the ugliness. # We need the "explode" key to be at the parameters level, not at the schema level. explode = jsonschema.pop(sw.explode, None) + description = jsonschema.pop(sw.description, None) parameter = { sw.name: prop, @@ -334,6 +335,8 @@ def _convert_schema_to_list_of_parameters(self, schema, converter, in_): if explode is not None: parameter[sw.explode] = explode + if description is not None: + parameter[sw.description] = description parameters.append(parameter) diff --git a/flask_rebar/utils/request_utils.py b/flask_rebar/utils/request_utils.py index d0c41877..a27f38ec 100644 --- a/flask_rebar/utils/request_utils.py +++ b/flask_rebar/utils/request_utils.py @@ -21,6 +21,7 @@ from flask_rebar import compat from flask_rebar import errors from flask_rebar import messages +from flask_rebar.utils.defaults import USE_DEFAULT class HeadersProxy(compat.Mapping): @@ -92,7 +93,7 @@ def normalize_schema(schema): This allows for either an instance of a marshmallow.Schema or the class itself to be passed to functions. """ - if not isinstance(schema, marshmallow.Schema): + if schema not in (None, USE_DEFAULT) and not isinstance(schema, marshmallow.Schema): schema = schema() return schema diff --git a/setup.py b/setup.py index 08d4fffe..fe3c3db2 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ if __name__ == "__main__": setup( name="flask-rebar", - version="1.10.0", + version="1.10.2", author="Barak Alon", author_email="barak.s.alon@gmail.com", description="Flask-Rebar combines flask, marshmallow, and swagger for robust REST services.", @@ -43,6 +43,7 @@ "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", "Topic :: Software Development :: Libraries :: Application Frameworks", diff --git a/tests/helpers.py b/tests/helpers.py index 51d3f900..abfe6e99 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -1,11 +1,10 @@ -import pytest - +import unittest from flask_rebar.compat import MARSHMALLOW_V2, MARSHMALLOW_V3 -skip_if_marshmallow_not_v2 = pytest.mark.skipif( +skip_if_marshmallow_not_v2 = unittest.skipIf( not MARSHMALLOW_V2, reason="Only applicable for Marshmallow version 2" ) -skip_if_marshmallow_not_v3 = pytest.mark.skipif( +skip_if_marshmallow_not_v3 = unittest.skipIf( not MARSHMALLOW_V3, reason="Only applicable for Marshmallow version 3" ) diff --git a/tests/swagger_generation/registries/exploded_query_string.py b/tests/swagger_generation/registries/exploded_query_string.py index 735e4c9b..4094cba1 100644 --- a/tests/swagger_generation/registries/exploded_query_string.py +++ b/tests/swagger_generation/registries/exploded_query_string.py @@ -13,7 +13,9 @@ class ExplodedQueryStringSchema(RequestSchema): - foos = QueryParamList(marshmallow.fields.String(), required=True) + foos = QueryParamList( + marshmallow.fields.String(), required=True, description="foo string" + ) @registry.handles( @@ -57,6 +59,7 @@ def get_foos(): "collectionFormat": "multi", "type": "array", "items": {"type": "string"}, + "description": "foo string", } ], } @@ -99,6 +102,7 @@ def get_foos(): "name": "foos", "in": "query", "required": True, + "description": "foo string", "schema": {"type": "array", "items": {"type": "string"}}, "explode": True, } diff --git a/tests/test_rebar.py b/tests/test_rebar.py index 28af1f95..8fd503d1 100644 --- a/tests/test_rebar.py +++ b/tests/test_rebar.py @@ -12,7 +12,6 @@ import marshmallow as m from flask import Flask -from werkzeug.routing import RequestRedirect from flask_rebar import messages from flask_rebar import HeaderApiKeyAuthenticator, SwaggerV3Generator @@ -677,3 +676,80 @@ def test_redirects_for_missing_trailing_slash(self): resp = app.test_client().get(path="/with_trailing_slash") self.assertIn(resp.status_code, (301, 308)) self.assertTrue(resp.headers["Location"].endswith("/with_trailing_slash/")) + + def test_bare_class_schemas_handled(self): + rebar = Rebar() + registry = rebar.create_handler_registry() + + expected_foo = FooSchema().load({"uid": "some_uid", "name": "Namey McNamerton"}) + expected_headers = {"x-name": "Header Name"} + + def get_foo(*args, **kwargs): + return expected_foo + + def post_foo(*args, **kwargs): + return expected_foo + + register_endpoint( + registry=registry, + method="GET", + path="/my_get_endpoint", + headers_schema=HeadersSchema, + response_body_schema={200: FooSchema}, + query_string_schema=FooListSchema, + func=get_foo, + ) + + register_endpoint( + registry=registry, + method="POST", + path="/my_post_endpoint", + request_body_schema=FooListSchema, + response_body_schema=FooSchema, + func=post_foo, + ) + + app = create_rebar_app(rebar) + # violate headers schema: + resp = app.test_client().get(path="/my_get_endpoint?name=QuerystringName") + self.assertEqual(resp.status_code, 400) + self.assertEqual( + get_json_from_resp(resp)["message"], messages.header_validation_failed + ) + # violate querystring schema: + resp = app.test_client().get(path="/my_get_endpoint", headers=expected_headers) + self.assertEqual(resp.status_code, 400) + self.assertEqual( + get_json_from_resp(resp)["message"], messages.query_string_validation_failed + ) + # valid request: + resp = app.test_client().get( + path="/my_get_endpoint?name=QuerystringName", headers=expected_headers + ) + self.assertEqual(resp.status_code, 200) + self.assertEqual(get_json_from_resp(resp), expected_foo.data) + + resp = app.test_client().post( + path="/my_post_endpoint", + data='{"wrong": "Posted Name"}', + content_type="application/json", + ) + self.assertEqual(resp.status_code, 400) + self.assertEqual( + get_json_from_resp(resp)["message"], messages.body_validation_failed + ) + + resp = app.test_client().post( + path="/my_post_endpoint", + data='{"name": "Posted Name"}', + content_type="application/json", + ) + self.assertEqual(resp.status_code, 200) + + # ensure Swagger generation doesn't break (Issue #115) + from flask_rebar import SwaggerV2Generator, SwaggerV3Generator + + swagger = SwaggerV2Generator().generate(registry) + self.assertIsNotNone(swagger) # really only care that it didn't barf + swagger = SwaggerV3Generator().generate(registry) + self.assertIsNotNone(swagger)