Repository: striglia/pyramid_swagger
Pull request #216 (2017-09-08 07:09:16)

pyramid_swagger renderer
While working with ``pyramid_swagger`` I noticed that the view responses are never marshaled via ``bravado-core`` methods. This means that if our specs are using ``format`` schema property the ``to_wire`` is never called (more info on [User-Defined Formats](http://bravado-core.readthedocs.io/en/latest/formats.html)).

Let's imagine that we have a view that returns a string with ``date`` format.
With the current (pre PR) pyramid_swagger the developer needs to enforce on the view the correct formatting of the object that represent the date (ie. ``datetime.date.today().isoformat()``), which could be annoying and different from what is happening with the input parameters (``request.swagger_data`` contains un-marshaled data, so the date string in the HTTP request will be converted to ``datetime.date`` object).
I already added the section in ``quickstart`` documentation page; on that page is available a longer description of this 😄 

The goal of this PR is:
 -  to define a new renderer (called ``pyramid_swagger``) that will perform view response marshaling and rendering with the default ``JSON`` renderer
 - to expose ``PyramidSwaggerRendererFactory`` class to *create* custom renderer factories which will operate on marshaled objects
 - to expose ``operation`` property in the request: gives access to ``bravado_core.operation.Operation`` in the view (#212) and allow ``PyramidSwaggerRendererFactory`` to properly marshal the response

NOTE: exposing ``operation`` property has "no overhead" because the property in injected by validator tween that already has evaluated the needed operation object (so the overhead is only caused by setting the property)


coveralls: 
[![Coverage Status](https://coveralls.io/builds/13180724/badge)](https://coveralls.io/builds/13180724)

Coverage increased (+0.02%) to 99.55% when pulling **d2b2dfe2e63980b84abbcd31776ab1fe0a11a2d1 on macisamuele:maci-pyramid_swagger-renderer** into **a99aaae7a6c2b2b41f512dd69392f61fb50a8dad on striglia:master**.

macisamuele: @striglia I have restructured a bit the documentation to make more easy to follow 😄 
Sorry for the really long delay on it.
coveralls: 
[![Coverage Status](https://coveralls.io/builds/13521473/badge)](https://coveralls.io/builds/13521473)

Coverage increased (+0.02%) to 99.55% when pulling **25fc16c9070bb39cec602b0374977e4bbd2c29a3 on macisamuele:maci-pyramid_swagger-renderer** into **a99aaae7a6c2b2b41f512dd69392f61fb50a8dad on striglia:master**.


Patch:
diff --git a/docs/changelog.rst b/docs/changelog.rst
index c447369..78df882 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -5,6 +5,8 @@ In Development
 ++++++++++++++++++++++++++
 * Support `include_missing_properties` bravado-core flag in pyramid configuration
 * Outsource flattening logic to bravado-core library.
+* Expose bravado-core ``operation`` in request object
+* Add ``pyramid_renderer`` and ``PyramidSwaggerRendererFactory``
 
 2.4.1 (2017-06-14)
 ++++++++++++++++++++++++++
diff --git a/docs/conf.py b/docs/conf.py
index c64d2c4..639cc11 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -91,7 +91,7 @@ pygments_style = 'sphinx'
 
 # The theme to use for HTML and HTML Help pages.  Major themes that come with
 # Sphinx are currently 'default' and 'sphinxdoc'.
-html_theme = 'sphinx_rtd_theme' if os.environ.get('READTHEDOCS') else 'classic'
+html_theme = 'sphinx_rtd_theme'
 
 # Theme options are theme-specific and customize the look and feel of a theme
 # further.  For a list of options available for each theme, see the
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index cca0f5e..26e5455 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -144,3 +144,109 @@ Otherwise, models are represented as dicts.
 
     Values in ``request.swagger_data`` are only available if
     ``pyramid_swawgger.enable_request_validation`` is enabled.
+
+Accessing Swagger Operation
+---------------------------
+
+During the implementation of an endpoint you could eventually have need of accessing the Swagger Specs that defined that specific view.
+``pyramid_swagger`` will inject in the request object a new property (that will be evaluated only if accessed) called ``operation``.
+
+``request.operation`` will be set to ``None`` for Swagger 1.2 defined endpoints, while it will be an `Operation <https://github.com/Yelp/bravado-core/blob/v4.8.4/bravado_core/operation.py#L12>`_ object if the endpoint is defined by Swagger 2.0 specs.
+
+pyramid_swagger renderer
+------------------------
+
+Using :mod:`pyramid_swagger` you will get automatic conversions of the input JSON objects to easy to handle python objects.
+An example could be a swagger object string property using the ``date`` format , the library will take care of converting the
+ISO 8601 date representation to an easy to handle python ``datetime.date`` object.
+
+While defining the :mod:`pyramid` view that will handle the endpoint you have to make sure that the chosen renderer will be able to
+properly render your response. In the case of an endpoint that returns *objects* that requires a special handling
+(like ``datetime.date``) the developer is *forced* to:
+
+* manually convert the python object to an object that could be handled by the renderer
+* add an `adapter <https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/renderers.html#using-the-add-adapter-method-of-a-custom-json-renderer>`_ for instructing pyramid to handle your object
+* define a custom renderer that is able to properly serialize the object
+
+:mod:`pyramid_swagger` provides:
+
+* a new renderer, called ``pyramid_swagger``
+* a new renderer renderer factory, called ``pyramid_swagger.renderer.PyramidSwaggerRendererFactory``
+
+----------------------------------
+How pyramid_swagger renderer works
+----------------------------------
+
+The new :mod:`pyramid_swagger` renderer is a wrapper around the default ``pyramid.renderers.JSON`` renderer.
+
+:mod:`pyramid_swagger` will receive, from your pyramid view, the object that has to be rendered, perform the marshaling operations and then call the default JSON renderer.
+
+.. note::
+    The usage of this renderer allows to get full support of `custom formats <configuration.html#user-formats-swagger-2-0-only>`_
+
+
+Let's assume that your view returns ``{'date': datetime.date.today()}`` and that your response spec is similar to
+
+.. code-block:: json
+
+    {
+        "200": {
+            "description": "HTTP/200",
+            "schema": {
+                "properties": {
+                    "date": {
+                        "type": "string",
+                        "format": "date"
+                    }
+                }
+            }
+        }
+    }
+
+
+If your view is configured to use ``json`` renderer then your endpoint will surprisingly return HTTP/500 errors.
+The errors are caused by the fact that ``pyramid.renderers.JSON`` is not aware on how to convert a ``datetime.date`` object.
+
+If your view is configured to use ``pyramid_swagger`` renderer then your endpoint will provide HTTP/200 responses similar
+to ``{"date": "2017-09-16"}``.
+
+This is possible because the marsharling of the view return value converts the ``datetime.date`` object to its ISO 8601
+string representation that could be handled by the default JSON renderer.
+
+.. note::
+
+    The marshaling operation will be performed according to the specific response schema defined for the particular endpoint.
+    It means that if your response doesn't specify a field it will be transparently passed to the wrapped renderer.
+
+
+---------------------------------------
+How PyramidSwaggerRendererFactory works
+---------------------------------------
+
+``PyramidSwaggerRendererFactory`` allows you to create a custom renderer that operates on the marshaled result from your view.
+
+The defined renderer will operate on the marshaled, according to the Swagger Specification, response.
+
+Example of definition of a custom renderer
+
+.. code-block:: python
+
+    class MyPersonalRendererFactory(object):
+        def __init__(self, info):
+            # Initialize your factory (refer to standard documentation for more information)
+            pass
+
+        def __call__(self, value, system):
+            # ``value`` contain the marshaled representation of the object returned by your view.
+            # If your view is returning a ``datetime.date`` object for a field with date format
+            # you can assume that the field has already been converted to its ISO 8601 representation
+
+            # perform your personal rendering operations
+            # you can assume that value is a marshaled response, so already JSON serializable object
+            return rendered_value
+
+Once you have defined your own renderer you have to wrap the new renderer in ``PyramidSwaggerRendererFactory`` and register it to the pyramid framework as described by `Adding and Changing Renderers pyramid documentation <https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/renderers.html#adding-and-changing-renderers>`_.
+
+.. code-block:: python
+
+    config.add_renderer(name='custom_renderer', factory=PyramidSwaggerRendererFactory(MyPersonalRendererFactory))
diff --git a/pyramid_swagger/__init__.py b/pyramid_swagger/__init__.py
index 23bb55e..8ea4c67 100644
--- a/pyramid_swagger/__init__.py
+++ b/pyramid_swagger/__init__.py
@@ -8,6 +8,7 @@ from .api import build_swagger_20_swagger_schema_views
 from .api import register_api_doc_endpoints
 from .ingest import get_swagger_schema
 from .ingest import get_swagger_spec
+from .renderer import PyramidSwaggerRendererFactory
 from .tween import get_swagger_versions
 from .tween import SWAGGER_12
 from .tween import SWAGGER_20
@@ -42,6 +43,8 @@ def includeme(config):
         under=pyramid.tweens.EXCVIEW
     )
 
+    config.add_renderer('pyramid_swagger', PyramidSwaggerRendererFactory())
+
     if settings.get('pyramid_swagger.enable_api_doc_views', True):
         if SWAGGER_12 in swagger_versions:
             register_api_doc_endpoints(
diff --git a/pyramid_swagger/renderer.py b/pyramid_swagger/renderer.py
new file mode 100644
index 0000000..dc8285b
--- /dev/null
+++ b/pyramid_swagger/renderer.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+"""
+This model contains the main factory of renderers to use while dealing with Swagger 2.0 endpoints.
+"""
+from functools import partial
+
+from bravado_core.exception import MatchingResponseNotFound
+from bravado_core.exception import SwaggerMappingError
+from bravado_core.marshal import marshal_schema_object
+from bravado_core.response import get_response_spec
+from pyramid.renderers import JSON
+
+
+class PyramidSwaggerRendererFactory(object):
+    def __init__(self, renderer_factory=JSON()):
+        self.renderer_factory = renderer_factory
+
+    def _marshal_object(self, request, response_object):
+        # operation attribute is injected by validator_tween in case the endpoint is served by Swagger 2.0 specs
+        operation = getattr(request, 'operation')
+
+        if not operation:
+            # If the request is not served by Swagger2.0 endpoint _marshal_object is NO_OP
+            return response_object
+
+        try:
+            response_spec = get_response_spec(
+                status_code=request.response.status_code,
+                op=request.operation,
+            )
+            return marshal_schema_object(
+                swagger_spec=request.registry.settings['pyramid_swagger.schema20'],
+                schema_object_spec=response_spec['schema'],
+                value=response_object,
+            )
+        except MatchingResponseNotFound:
+            # Response specs not found
+            return response_object
+        except SwaggerMappingError:
+            # marshaling process failed
+            return response_object
+
+    def _render(self, external_renderer, value, system):
+        value = self._marshal_object(system['request'], value)
+        return external_renderer(value, system)
+
+    def __call__(self, info):
+        return partial(self._render, self.renderer_factory(info))
diff --git a/pyramid_swagger/tween.py b/pyramid_swagger/tween.py
index 74f6bef..f75fc46 100644
--- a/pyramid_swagger/tween.py
+++ b/pyramid_swagger/tween.py
@@ -13,6 +13,7 @@ import jsonschema.exceptions
 import simplejson
 from bravado_core.exception import SwaggerMappingError
 from bravado_core.formatter import SwaggerFormat  # noqa
+from bravado_core.operation import Operation
 from bravado_core.request import IncomingRequest
 from bravado_core.request import unmarshal_request
 from bravado_core.response import get_response_spec
@@ -149,6 +150,8 @@ def validation_tween_factory(handler, registry):
     settings = load_settings(registry)
     route_mapper = registry.queryUtility(IRoutesMapper)
 
+    validation_context = _get_validation_context(registry)
+
     def validator_tween(request):
         # We don't have access to this yet but let's go ahead and build the
         # matchdict so we can validate it and use it to exclude routes from
@@ -160,8 +163,6 @@ def validation_tween_factory(handler, registry):
         if should_exclude_request(settings, request, route_info):
             return handler(request)
 
-        validation_context = _get_validation_context(registry)
-
         try:
             op_or_validators_map = swagger_handler.op_for_request(
                 request, route_info=route_info, spec=spec)
@@ -172,6 +173,11 @@ def validation_tween_factory(handler, registry):
             else:
                 return handler(request)
 
+        def operation(_):
+            return op_or_validators_map if isinstance(op_or_validators_map, Operation) else None
+
+        request.set_property(operation)
+
         if settings.validate_request:
             with validation_context(request, response=None):
                 request_data = swagger_handler.handle_request(
diff --git a/tox.ini b/tox.ini
index 5a2ddb6..b9047b6 100644
--- a/tox.ini
+++ b/tox.ini
@@ -19,6 +19,7 @@ ignore = E501
 [testenv:docs]
 deps =
     sphinx
+    sphinx-rtd-theme
 commands =
     sphinx-build -W -b html -d {envtmpdir}/doctrees docs docs/_build/html
     sphinx-build -W -b linkcheck docs docs/_build/html


Test patch:
diff --git a/tests/acceptance/app/__init__.py b/tests/acceptance/app/__init__.py
index fd374f3..f0a116e 100644
--- a/tests/acceptance/app/__init__.py
+++ b/tests/acceptance/app/__init__.py
@@ -1,4 +1,7 @@
 # -*- coding: utf-8 -*-
+import datetime
+
+import six
 import webob
 from pyramid.config import Configurator
 from pyramid.view import view_config
@@ -31,6 +34,19 @@ def sample(request):
     return {}
 
 
+@view_config(route_name='echo_date_json_renderer', request_method='POST', renderer='json')
+@view_config(route_name='echo_date', request_method='POST', renderer='pyramid_swagger')
+def date_view(request):
+
+    if '2.0' in request.registry.settings['pyramid_swagger.swagger_versions']:
+        # Swagger 2.0 endpoint handling
+        assert isinstance(request.swagger_data['body']['date'], datetime.date)
+    else:
+        assert isinstance(request.swagger_data['body']['date'], six.string_types)
+
+    return request.swagger_data['body']
+
+
 @view_config(route_name='swagger_undefined', renderer='json')
 def swagger_undefined(request):
     return {}
@@ -59,6 +75,9 @@ def main(global_config, **settings):
     config.add_route('throw_400', '/throw_400')
     config.add_route('swagger_undefined', '/undefined/path')
 
+    config.add_route('echo_date', '/echo_date')
+    config.add_route('echo_date_json_renderer', '/echo_date_json_renderer')
+
     config.scan()
     return config.make_wsgi_app()
 
diff --git a/tests/acceptance/request_test.py b/tests/acceptance/request_test.py
index a0141ba..5ca313e 100644
--- a/tests/acceptance/request_test.py
+++ b/tests/acceptance/request_test.py
@@ -1,4 +1,5 @@
 # -*- coding: utf-8 -*-
+import datetime
 from contextlib import contextmanager
 
 import pytest
@@ -24,6 +25,7 @@ def test_app(request, **overrides):
         'pyramid_swagger.swagger_versions': request.param},
         **overrides
     )
+
     return App(main({}, **settings))
 
 
@@ -35,9 +37,8 @@ def validation_context(request, response=None):
         exceptions.RequestValidationError,
         exceptions.ResponseValidationError,
         exceptions.PathNotFoundError,
-    ) as e:
+    ):
         raise exception_response(206)
-        assert e.child is not None
     except Exception:
         raise exception_response(400)
 
@@ -45,6 +46,39 @@ def validation_context(request, response=None):
 validation_ctx_path = 'tests.acceptance.request_test.validation_context'
 
 
+def test_echo_date_with_pyramid_swagger_renderer(test_app):
+    input_object = {'date': datetime.date.today().isoformat()}
+
+    response = test_app.post_json('/echo_date', input_object)
+
+    # If the request is served via Swagger1.2
+    assert response.status_code == 200
+    assert response.json == input_object
+
+
+def test_echo_date_with_json_renderer(test_app):
+    today = datetime.date.today()
+    input_object = {'date': today.isoformat()}
+
+    exc = None
+    response = None
+    try:
+        response = test_app.post_json('/echo_date_json_renderer', input_object)
+    except TypeError as exception:
+        exc = exception
+
+    served_swagger_versions = test_app.app.registry.settings['pyramid_swagger.swagger_versions']
+
+    if '2.0' in served_swagger_versions:
+        # If the request is served via Swagger2.0, pyramid_swagger will perform types
+        # conversions providing a datetime.date object in the pyramid view
+        assert exc.args == ('{!r} is not JSON serializable'.format(today), )
+    else:
+        # If the request is served via Swagger1.2 there are no implicit type conversions performed by pyramid_swagger
+        assert response.status_code == 200
+        assert response.json == input_object
+
+
 def test_200_with_form_params(test_app):
     assert test_app.post(
         '/post_with_form_params',
diff --git a/tests/ingest_test.py b/tests/ingest_test.py
index b577460..a06950d 100644
--- a/tests/ingest_test.py
+++ b/tests/ingest_test.py
@@ -56,7 +56,7 @@ def test_get_swagger_schema_default():
     }
 
     swagger_schema = get_swagger_schema(settings)
-    assert len(swagger_schema.pyramid_endpoints) == 4
+    assert len(swagger_schema.pyramid_endpoints) == 5
     assert swagger_schema.resource_validators
 
 
@@ -80,6 +80,7 @@ def test_generate_resource_listing():
     expected = {
         'swaggerVersion': 1.2,
         'apis': [
+            {'path': '/echo_date'},
             {'path': '/no_models'},
             {'path': '/other_sample'},
             {'path': '/sample'},
diff --git a/tests/renderer_test.py b/tests/renderer_test.py
new file mode 100644
index 0000000..5262cf1
--- /dev/null
+++ b/tests/renderer_test.py
@@ -0,0 +1,252 @@
+# -*- coding: utf-8 -*-
+import datetime
+import json
+
+import pytest
+from bravado_core.exception import MatchingResponseNotFound
+from bravado_core.exception import SwaggerMappingError
+from bravado_core.operation import Operation
+from bravado_core.spec import Spec
+from mock import mock
+from pyramid.testing import DummyRequest
+
+from pyramid_swagger import PyramidSwaggerRendererFactory
+from pyramid_swagger import renderer
+
+
+class TestPyramidSwaggerRendererFactoryUnitTest(object):
+
+    @pytest.yield_fixture
+    def mock_marshal_schema_object(self):
+        with mock.patch('pyramid_swagger.renderer.marshal_schema_object') as _mock:
+            yield _mock
+
+    @pytest.yield_fixture
+    def mock_get_response_spec(self):
+        with mock.patch('pyramid_swagger.renderer.get_response_spec') as _mock:
+            yield _mock
+
+    def setup_method(self, method):
+        self.info = mock.Mock(name='info')
+        self.value_to_render = mock.Mock(name='value_to_render')
+        self.mock_spec = mock.Mock(spec=Spec)
+        self.mock_request = DummyRequest(
+            swagger_data=mock.Mock(spec=dict),
+            operation=mock.Mock(spec=Operation),
+        )
+        self.mock_request.registry.settings = {'pyramid_swagger.schema20': self.mock_spec}
+        self.mock_system = {'request': self.mock_request}
+        self.external_renderer_factory = mock.Mock(name='external_renderer_factory')
+
+    def test_successiful_rendering_flow(self, mock_get_response_spec, mock_marshal_schema_object):
+        renderer_factory = PyramidSwaggerRendererFactory(renderer_factory=self.external_renderer_factory)
+
+        renderer = renderer_factory(info=self.info)
+        rendered_value = renderer(self.value_to_render, self.mock_system)
+
+        self.external_renderer_factory.assert_called_once_with(self.info)
+        mock_get_response_spec.assert_called_once_with(
+            op=self.mock_request.operation,
+            status_code=self.mock_request.response.status_code,
+        )
+        mock_marshal_schema_object.assert_called_once_with(
+            schema_object_spec=mock_get_response_spec.return_value.__getitem__.return_value,
+            swagger_spec=self.mock_spec,
+            value=self.value_to_render,
+        )
+        self.external_renderer_factory.return_value.assert_called_once_with(
+            mock_marshal_schema_object.return_value,
+            self.mock_system,
+        )
+        assert rendered_value == self.external_renderer_factory.return_value.return_value
+
+    def test_rendering_operation_not_found(self, mock_get_response_spec, mock_marshal_schema_object):
+        renderer_factory = PyramidSwaggerRendererFactory(renderer_factory=self.external_renderer_factory)
+        self.mock_request.operation = None
+
+        renderer = renderer_factory(info=self.info)
+        rendered_value = renderer(self.value_to_render, self.mock_system)
+
+        self.external_renderer_factory.assert_called_once_with(self.info)
+        assert not mock_get_response_spec.called
+        assert not mock_marshal_schema_object.called
+        self.external_renderer_factory.return_value.assert_called_once_with(
+            self.value_to_render,
+            self.mock_system,
+        )
+        assert rendered_value == self.external_renderer_factory.return_value.return_value
+
+    def test_rendering_response_spec_not_found(self, mock_get_response_spec, mock_marshal_schema_object):
+        renderer_factory = PyramidSwaggerRendererFactory(renderer_factory=self.external_renderer_factory)
+        mock_get_response_spec.side_effect = MatchingResponseNotFound
+
+        renderer = renderer_factory(info=self.info)
+        rendered_value = renderer(self.value_to_render, self.mock_system)
+
+        self.external_renderer_factory.assert_called_once_with(self.info)
+        mock_get_response_spec.assert_called_once_with(
+            op=self.mock_request.operation,
+            status_code=self.mock_request.response.status_code,
+        )
+        assert not mock_marshal_schema_object.called
+        self.external_renderer_factory.return_value.assert_called_once_with(
+            self.value_to_render,
+            self.mock_system,
+        )
+        assert rendered_value == self.external_renderer_factory.return_value.return_value
+
+    def test_rendering_error_during_marshaling(self, mock_get_response_spec, mock_marshal_schema_object):
+        renderer_factory = PyramidSwaggerRendererFactory(renderer_factory=self.external_renderer_factory)
+        mock_marshal_schema_object.side_effect = SwaggerMappingError
+
+        renderer = renderer_factory(info=self.info)
+        rendered_value = renderer(self.value_to_render, self.mock_system)
+
+        self.external_renderer_factory.assert_called_once_with(self.info)
+        mock_get_response_spec.assert_called_once_with(
+            op=self.mock_request.operation,
+            status_code=self.mock_request.response.status_code,
+        )
+        mock_marshal_schema_object.assert_called_once_with(
+            schema_object_spec=mock_get_response_spec.return_value.__getitem__.return_value,
+            swagger_spec=self.mock_spec,
+            value=self.value_to_render,
+        )
+        self.external_renderer_factory.return_value.assert_called_once_with(
+            self.value_to_render,
+            self.mock_system,
+        )
+        assert rendered_value == self.external_renderer_factory.return_value.return_value
+
+
+class TestPyramidSwaggerRendererFactoryIntegrationTest(object):
+    swagger_spec_dict = {
+        'swagger': '2.0',
+        'info': {
+            'title': 'A title',
+            'version': '0.0.0',
+        },
+        'paths': {
+            '/endpoint': {
+                'get': {
+                    'responses': {
+                        '200': {
+                            'description': 'HTTP/200 OK',
+                            'schema': {
+                                'properties': {
+                                    'date': {
+                                        'type': 'string',
+                                        'format': 'date',
+                                    }
+                                },
+                                'type': 'object',
+                            },
+                        },
+                    },
+                },
+            },
+        },
+    }
+
+    @pytest.yield_fixture(scope='session')
+    def swagger_spec(self):
+        spec = Spec.from_dict(self.swagger_spec_dict)
+        yield spec
+
+    @pytest.yield_fixture
+    def mock_request(self, swagger_spec):
+        mock_request = DummyRequest(
+            swagger_data={},
+            operation=swagger_spec.resources['endpoint'].get_endpoint,
+        )
+        mock_request.registry.settings = {'pyramid_swagger.schema20': swagger_spec}
+        yield mock_request
+
+    @pytest.yield_fixture
+    def spy_marshal_schema_object(self):
+        with mock.patch.object(renderer, 'marshal_schema_object', wraps=renderer.marshal_schema_object) as _mock:
+            yield _mock
+
+    @pytest.yield_fixture
+    def spy_get_response_spec(self):
+        with mock.patch.object(renderer, 'get_response_spec', wraps=renderer.get_response_spec) as _mock:
+            yield _mock
+
+    def setup_method(self, method):
+        self.info = mock.Mock(name='info')
+        self.renderer_factory = PyramidSwaggerRendererFactory()
+
+    @pytest.mark.parametrize(
+        'view_response, expected_rendered_value',
+        [
+            [None, 'null'],
+            [{}, '{}'],
+            [{'new_property': 'any value'}, '{"new_property": "any value"}'],
+            [{'date': datetime.date.today()}, '{"date": "' + datetime.date.today().isoformat() + '"}'],
+        ],
+    )
+    def test_no_errors(self, spy_get_response_spec, spy_marshal_schema_object, swagger_spec, mock_request, view_response, expected_rendered_value):
+        system = {'request': mock_request}
+        renderer = self.renderer_factory(info=self.info)
+        rendered_value = renderer(view_response, system)
+
+        spy_get_response_spec.assert_called_once_with(
+            op=mock_request.operation,
+            status_code=mock_request.response.status_code,
+        )
+        spy_marshal_schema_object.assert_called_once_with(
+            schema_object_spec=self.swagger_spec_dict['paths']['/endpoint']['get']['responses']['200']['schema'],
+            swagger_spec=swagger_spec,
+            value=view_response,
+        )
+
+        assert rendered_value == expected_rendered_value
+
+    @pytest.mark.parametrize(
+        'view_response, expect_exernal_renderer_exception',
+        [
+            [None, False],
+            [{}, False],
+            [{'new_property': 'any value'}, False],
+            [{'date': datetime.date.today()}, True],
+        ],
+    )
+    def test_response_spec_not_found(self, spy_get_response_spec, spy_marshal_schema_object, mock_request, view_response, expect_exernal_renderer_exception):
+        mock_request.response.status_code = 400
+        system = {'request': mock_request}
+        renderer = self.renderer_factory(info=self.info)
+
+        rendered_value = None
+        expected_rendered_value = None
+        if expect_exernal_renderer_exception:
+            with pytest.raises(Exception):
+                # Expect exception because datetime.date object cannot be serialized by JSON pyramid renderer
+                rendered_value = renderer(view_response, system)
+        else:
+            rendered_value = renderer(view_response, system)
+            expected_rendered_value = json.dumps(view_response)
+
+        spy_get_response_spec.assert_called_once_with(
+            op=mock_request.operation,
+            status_code=mock_request.response.status_code,
+        )
+        assert not spy_marshal_schema_object.called
+        assert rendered_value == expected_rendered_value
+
+    def test_marshaling_raise_exception(self, spy_get_response_spec, spy_marshal_schema_object, swagger_spec, mock_request):
+        system = {'request': mock_request}
+        value_to_renderer = {'date': datetime.date.today().isoformat()}
+
+        renderer = self.renderer_factory(info=self.info)
+        rendered_value = renderer(value_to_renderer, system)
+
+        spy_get_response_spec.assert_called_once_with(
+            op=mock_request.operation,
+            status_code=mock_request.response.status_code,
+        )
+        spy_marshal_schema_object.assert_called_once_with(
+            schema_object_spec=self.swagger_spec_dict['paths']['/endpoint']['get']['responses']['200']['schema'],
+            swagger_spec=swagger_spec,
+            value=value_to_renderer,
+        )
+        assert rendered_value == json.dumps(value_to_renderer)
diff --git a/tests/sample_schemas/good_app/api_docs.json b/tests/sample_schemas/good_app/api_docs.json
index 80824a6..dc6a896 100644
--- a/tests/sample_schemas/good_app/api_docs.json
+++ b/tests/sample_schemas/good_app/api_docs.json
@@ -12,6 +12,10 @@
         {
             "path": "/other_sample",
             "description": "Two heads are better than one."
+        },
+        {
+            "path": "/echo_date",
+            "description": "Echoes the input body in the response. Endpoint used for verifying PyramidSwaggerRendererFactory"
         }
     ]
 }
diff --git a/tests/sample_schemas/good_app/echo_date.json b/tests/sample_schemas/good_app/echo_date.json
new file mode 100644
index 0000000..860be68
--- /dev/null
+++ b/tests/sample_schemas/good_app/echo_date.json
@@ -0,0 +1,56 @@
+{
+  "apiVersion": "0.1",
+  "swaggerVersion": "1.2",
+  "basePath": "http://localhost:9999/echo_date",
+  "apis": [
+    {
+      "path": "/echo_date",
+      "operations": [
+        {
+          "method": "POST",
+          "nickname": "echo_date",
+          "type": "string",
+          "parameters": [
+            {
+              "name": "body",
+              "paramType": "body",
+              "type": "object_with_formats"
+            }
+          ]
+        }
+      ]
+    },
+    {
+      "path": "/echo_date_json_renderer",
+      "operations": [
+        {
+          "description": "This endpoint is used in tests/acceptance/request_test.py and requires to be identical to /echo_date endpoint",
+          "method": "POST",
+          "nickname": "echo_date",
+          "parameters": [
+            {
+              "name": "body",
+              "paramType": "body",
+              "type": "object_with_formats"
+            }
+          ],
+          "type": "string"
+        }
+      ]
+    }
+  ],
+  "models": {
+    "object_with_formats": {
+      "id": "object_with_formats",
+      "properties": {
+        "date": {
+          "type": "string",
+          "format": "date"
+        }
+      },
+      "required": [
+        "date"
+      ]
+    }
+  }
+}
diff --git a/tests/sample_schemas/good_app/swagger.json b/tests/sample_schemas/good_app/swagger.json
index f65eecc..8fe26e0 100644
--- a/tests/sample_schemas/good_app/swagger.json
+++ b/tests/sample_schemas/good_app/swagger.json
@@ -17,6 +17,51 @@
         "operationId": "no_models_get"
       }
     },
+    "/echo_date": {
+      "post": {
+        "parameters": [
+          {
+            "in": "body",
+            "name": "body",
+            "schema": {
+              "$ref": "#/definitions/object_with_formats"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "HTTP/200 OK",
+            "schema": {
+              "$ref": "#/definitions/object_with_formats"
+            }
+          }
+        },
+        "operationId": "echo_date"
+      }
+    },
+    "/echo_date_json_renderer": {
+      "post": {
+        "description": "This endpoint is used in tests/acceptance/request_test.py and requires to be identical to /echo_date endpoint",
+        "parameters": [
+          {
+            "in": "body",
+            "name": "body",
+            "schema": {
+              "$ref": "#/definitions/object_with_formats"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "HTTP/200 OK",
+            "schema": {
+              "$ref": "#/definitions/object_with_formats"
+            }
+          }
+        },
+        "operationId": "echo_date_json_renderer"
+      }
+    },
     "/sample/{path_arg}/resource": {
       "get": {
         "responses": {
@@ -254,6 +299,18 @@
     "http"
   ],
   "definitions": {
+    "object_with_formats": {
+      "properties": {
+        "date": {
+          "type": "string",
+          "format": "date"
+        }
+      },
+      "required": [
+        "date"
+      ],
+      "type": "object"
+    },
     "standard_response": {
       "type": "object",
       "required": [