From 7a8f195712cf0f25f2a1830761014747273284a9 Mon Sep 17 00:00:00 2001 From: Correl Date: Fri, 5 Mar 2021 15:23:23 -0500 Subject: [PATCH] Allow overriding of spec as a dictionary --- docs/conf.py | 7 ++- docs/handler.rst | 4 -- docs/testing.rst | 3 -- tests/test_handler.py | 85 +++++++++++++++++-------------------- tests/test_testing.py | 51 +++++++++++----------- tornado_openapi3/handler.py | 27 +++++++++++- tornado_openapi3/testing.py | 28 ++++++++++-- 7 files changed, 120 insertions(+), 85 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 0faee37..040572e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -29,10 +29,15 @@ author = "Correl Roush" # ones. extensions = [ "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", "sphinx_rtd_theme", ] -autodoc_member_order = "groupwise" +autodoc_member_order = "bysource" +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + "tornado": ("https://tornado.readthedocs.org/", None), +} # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] diff --git a/docs/handler.rst b/docs/handler.rst index 6eb6848..bf52d7c 100644 --- a/docs/handler.rst +++ b/docs/handler.rst @@ -1,9 +1,5 @@ Handling Incoming Requests ========================== -OpenAPIRequestHandler extends Tornado's RequestHandler class, providing -validation of incoming requests and translating errors into appropriate HTTP -responses. - .. automodule:: tornado_openapi3.handler :members: diff --git a/docs/testing.rst b/docs/testing.rst index c94dd87..4baf5ca 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -1,8 +1,5 @@ Testing API Responses ===================== -AsyncOpenAPITestCase extends Tornado's AsyncHTTPTestCase class, providing -validation of the responses from your application and raising errors in tests. - .. automodule:: tornado_openapi3.testing :members: diff --git a/tests/test_handler.py b/tests/test_handler.py index b7e6229..b88dbbc 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -1,7 +1,6 @@ import json import unittest.mock -from openapi_core import create_spec # type: ignore from openapi_core.exceptions import OpenAPIError # type: ignore import tornado.httpclient # type: ignore import tornado.web # type: ignore @@ -11,60 +10,56 @@ from tornado_openapi3.handler import OpenAPIRequestHandler class ResourceHandler(OpenAPIRequestHandler): - spec = create_spec( - { - "openapi": "3.0.0", - "info": { - "title": "Test API", - "version": "1.0.0", + spec_dict = { + "openapi": "3.0.0", + "info": { + "title": "Test API", + "version": "1.0.0", + }, + "components": { + "schemas": { + "resource": { + "type": "object", + "properties": {"name": {"type": "string"}}, + "required": ["name"], + }, }, - "components": { - "schemas": { - "resource": { - "type": "object", - "properties": {"name": {"type": "string"}}, - "required": ["name"], + "securitySchemes": { + "basicAuth": { + "type": "http", + "scheme": "bearer", + } + }, + }, + "security": [{"basicAuth": []}], + "paths": { + "/resource": { + "post": { + "requestBody": { + "required": True, + "content": { + "application/vnd.example.resource+json": { + "schema": {"$ref": "#/components/schemas/resource"}, + } + }, }, - }, - "securitySchemes": { - "basicAuth": { - "type": "http", - "scheme": "bearer", - } - }, - }, - "security": [{"basicAuth": []}], - "paths": { - "/resource": { - "post": { - "requestBody": { - "required": True, + "responses": { + "200": { + "description": "Success", "content": { "application/vnd.example.resource+json": { "schema": {"$ref": "#/components/schemas/resource"}, } }, }, - "responses": { - "200": { - "description": "Success", - "content": { - "application/vnd.example.resource+json": { - "schema": { - "$ref": "#/components/schemas/resource" - }, - } - }, - }, - "401": { - "description": "Missing or invalid credentials", - }, + "401": { + "description": "Missing or invalid credentials", }, - } + }, } - }, - } - ) + } + }, + } custom_media_type_deserializers = { "application/vnd.example.resource+json": json.loads, } diff --git a/tests/test_testing.py b/tests/test_testing.py index b0fa172..445ec70 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -1,6 +1,5 @@ import json -from openapi_core import create_spec # type: ignore from openapi_core.schema.responses.exceptions import InvalidResponse # type: ignore import tornado.web # type: ignore @@ -11,31 +10,29 @@ from tornado_openapi3.testing import AsyncOpenAPITestCase def spec(responses: dict = dict()) -> dict: if not responses: responses = {"200": {"description": "Success"}} - return create_spec( - { - "openapi": "3.0.0", - "info": { - "title": "Test API", - "version": "1.0.0", - }, - "components": { - "schemas": { - "resource": { - "type": "object", - "properties": {"name": {"type": "string"}}, - "required": ["name"], - }, + return { + "openapi": "3.0.0", + "info": { + "title": "Test API", + "version": "1.0.0", + }, + "components": { + "schemas": { + "resource": { + "type": "object", + "properties": {"name": {"type": "string"}}, + "required": ["name"], }, }, - "paths": { - "/resource": { - "get": { - "responses": responses, - } + }, + "paths": { + "/resource": { + "get": { + "responses": responses, } - }, - } - ) + } + }, + } class TestTestCase(AsyncOpenAPITestCase): @@ -54,7 +51,7 @@ class TestTestCase(AsyncOpenAPITestCase): class BaseTestCase(AsyncOpenAPITestCase): - spec = spec() + spec_dict = spec() custom_media_type_deserializers = { "application/vnd.example.resource+json": json.loads, } @@ -76,7 +73,7 @@ class BaseTestCase(AsyncOpenAPITestCase): class SuccessTests(BaseTestCase): - spec = spec( + spec_dict = spec( responses={ "200": { "description": "Success", @@ -99,7 +96,7 @@ class SuccessTests(BaseTestCase): class IncorrectResponseTests(BaseTestCase): - spec = spec(responses={"200": {"description": "Success"}}) + spec_dict = spec(responses={"200": {"description": "Success"}}) async def get(self, handler: tornado.web.RequestHandler) -> None: handler.set_status(400) @@ -111,7 +108,7 @@ class IncorrectResponseTests(BaseTestCase): class RaiseErrorTests(BaseTestCase): - spec = spec( + spec_dict = spec( responses={ "500": { "description": "An error has occurred.", diff --git a/tornado_openapi3/handler.py b/tornado_openapi3/handler.py index 2da1ef9..fd9c04a 100644 --- a/tornado_openapi3/handler.py +++ b/tornado_openapi3/handler.py @@ -1,6 +1,7 @@ import asyncio import logging +from openapi_core import create_spec # type: ignore from openapi_core.exceptions import OpenAPIError # type: ignore from openapi_core.deserializing.exceptions import DeserializeError # type: ignore from openapi_core.schema.specs.models import Spec # type: ignore @@ -21,16 +22,38 @@ logger = logging.getLogger(__name__) class OpenAPIRequestHandler(tornado.web.RequestHandler): + """Base class for HTTP request handlers. + + A request handler extending :py:class:`tornado.web.RequestHandler` providing + OpenAPI spec validation on incoming requests and translating errors into + appropriate HTTP responses. + + """ + @property - def spec(self) -> Spec: - """The OpenAPI 3 specification. + def spec_dict(self) -> dict: + """The OpenAPI 3 specification Override this in your request handlers to load or define your OpenAPI 3 spec. + :rtype: dict + """ raise NotImplementedError() + @property + def spec(self) -> Spec: + """The OpenAPI 3 specification. + + Override this in your request handlers to customize how your OpenAPI 3 + spec is loaded and validated. + + :rtype: :py:class:`openapi_core.schema.specs.model.Spec` + + """ + return create_spec(self.spec_dict, validate_spec=False) + @property def custom_media_type_deserializers(self) -> dict: """A dictionary mapping media types to deserializing functions. diff --git a/tornado_openapi3/testing.py b/tornado_openapi3/testing.py index 2c904e4..79d6222 100644 --- a/tornado_openapi3/testing.py +++ b/tornado_openapi3/testing.py @@ -3,20 +3,42 @@ from typing import Any import tornado.httpclient # type: ignore import tornado.testing # type: ignore +from openapi_core import create_spec # type: ignore from openapi_core.schema.specs.models import Spec # type: ignore from tornado_openapi3.responses import ResponseValidator class AsyncOpenAPITestCase(tornado.testing.AsyncHTTPTestCase): + """A test case that starts up an HTTP server. + + An async test case extending :py:class:`tornado.testing.AsyncHTTPTestCase`, + providing OpenAPI spec validation on the responses from your application and + raising errors in tests. + + """ + + @property + def spec_dict(self) -> dict: + """The OpenAPI 3 specification + + Override this in your test cases to load or define your OpenAPI 3 spec. + + :rtype: dict + + """ + raise NotImplementedError() + @property def spec(self) -> Spec: """The OpenAPI 3 specification. - Override this in your request handlers to load or define your OpenAPI 3 - spec. + Override this in your test cases to customize how your OpenAPI 3 spec is + loaded and validated. + + :rtype: :py:class:`openapi_core.schema.specs.model.Spec` """ - raise NotImplementedError() + return create_spec(self.spec_dict) @property def custom_media_type_deserializers(self) -> dict: