Repository: striglia/pyramid_swagger (python)

Issue:
Swagger 2.0 validation errors don't raise RequestValidationError
Here the validation_context is inside the validation_error decorated function so any raised issues aren't caught and made generic for handling by the validation context.

```python
@validation_error(RequestValidationError)
def swaggerize_request(request, op, **kwargs):
    validation_context = kwargs['validation_context']
    with validation_context(request):
        request_data = unmarshal_request(request, op)
    return request_data
```

Pull request description:
Fix #195 and include more information in ValidationErrors
fixed validation error issue, updated test for regression and added more information for exception handling. 

Updated acceptance test to only 206 on expected errors and the validation_context test started failing. This should help prevent regression in the future.

Patch:
diff --git a/.travis.yml b/.travis.yml
index 907bdb5..b607e39 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,12 +1,12 @@
 sudo: false
 language: python
-matrix:
-    include:
-    - env: TOXENV=py27
-    - python: "3.5"
-      env: TOXENV=py35
-    - env: TOXENV=py27-pyramid15
-    - env: TOXENV=docs
-install: pip install tox coveralls
-script: tox
+env:
+- TOX_ENV=py27
+- TOX_ENV=py34
+- TOX_ENV=py27-pyramid15
+- TOX_ENV=docs
+install:
+- pip install --use-mirrors tox==1.9.2 coveralls
+script:
+- tox -e $TOX_ENV
 after_success: coveralls
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 27b3756..517dee8 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1,5 +1,11 @@
 Changelog
 =========
+2.3.2 (2017-03-27)
+++++++++++++++++++
+* Fix validation context for swagger 2.0 requests
+* Added docs for validation context
+* Original exception is accessible to validation context exceptions
+
 2.3.1 (2017-XX-XX)
 ++++++++++++++++++
 * Remove support for Python 2.6, newer Pyramid versions don't support it either
diff --git a/docs/configuration.rst b/docs/configuration.rst
index 2fdf74b..a1d07fa 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -156,6 +156,43 @@ After defining this format, it can be used in the Swagger Spec definition like s
 
 .. _Data Types: https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#user-content-data-types
 
+validation_context_path
+-----------------------
+
+Formatting validation errors for API requests/responses to fit every possible
+swagger spec and response type is very complicated and will never cover every
+scenario. The ``validation_context_path`` option provides a way to change or
+format the response returned when :mod:`pyramid_swagger` validation fails.
+
+Sample usage:
+
+.. code-block:: python
+
+        from pyramid_swagger import exceptions
+
+        class UserDefinedResponseError(Exception):
+            pass
+
+        def validation_context(request, response=None):
+            try:
+                yield
+            except exceptions.RequestValidationError as e:
+                # Content type will be application/json instead of plain/text
+                raise exceptions.RequestValidationError(json=[str(e)])
+
+            except exceptions.ResponseValidationError as e:
+                # Reraise as non-pyramid exception
+                raise UserDefinedResponseError(str(e))
+
+
+
+The errors that are raised from the validation_context are defined in
+:mod:`pyramid_swagger.exceptions`.
+
+.. note::
+
+    By default :mod:`pyramid_swagger` validation errors return content type plain/text
+
 generate_resource_listing (Swagger 1.2 only)
 --------------------------------------------
 
diff --git a/pyramid_swagger/exceptions.py b/pyramid_swagger/exceptions.py
index 20b5b9a..3497c95 100644
--- a/pyramid_swagger/exceptions.py
+++ b/pyramid_swagger/exceptions.py
@@ -8,15 +8,21 @@ from pyramid.httpexceptions import HTTPNotFound
 
 
 class RequestValidationError(HTTPBadRequest):
-    pass
+    def __init__(self, *args, **kwargs):
+        self.child = kwargs.pop('child', None)
+        super(RequestValidationError, self).__init__(*args, **kwargs)
 
 
 class PathNotFoundError(HTTPNotFound):
-    pass
+    def __init__(self, *args, **kwargs):
+        self.child = kwargs.pop('child', None)
+        super(PathNotFoundError, self).__init__(*args, **kwargs)
 
 
 class ResponseValidationError(HTTPInternalServerError):
-    pass
+    def __init__(self, *args, **kwargs):
+        self.child = kwargs.pop('child', None)
+        super(ResponseValidationError, self).__init__(*args, **kwargs)
 
 
 def wrap_exception(exception_class):
diff --git a/pyramid_swagger/tween.py b/pyramid_swagger/tween.py
index c14cea0..83d9b33 100644
--- a/pyramid_swagger/tween.py
+++ b/pyramid_swagger/tween.py
@@ -166,15 +166,16 @@ def validation_tween_factory(handler, registry):
         except PathNotMatchedError as exc:
             if settings.validate_path:
                 with validation_context(request):
-                    raise PathNotFoundError(str(exc))
+                    raise PathNotFoundError(str(exc), child=exc)
             else:
                 return handler(request)
 
         if settings.validate_request:
-            request_data = swagger_handler.handle_request(
-                PyramidSwaggerRequest(request, route_info),
-                op_or_validators_map,
-                validation_context=validation_context)
+            with validation_context(request, response=None):
+                request_data = swagger_handler.handle_request(
+                    PyramidSwaggerRequest(request, route_info),
+                    op_or_validators_map,
+                )
 
             def swagger_data(_):
                 return request_data
@@ -309,7 +310,7 @@ class PyramidSwaggerResponse(OutgoingResponse):
         return getattr(self.response, 'json_body', {})
 
 
-def handle_request(request, validator_map, validation_context, **kwargs):
+def handle_request(request, validator_map, **kwargs):
     """Validate the request against the swagger spec and return a dict with
     all parameter values available in the request, casted to the expected
     python type.
@@ -317,10 +318,10 @@ def handle_request(request, validator_map, validation_context, **kwargs):
     :param request: a :class:`PyramidSwaggerRequest` to validate
     :param validator_map: a :class:`pyramid_swagger.load_schema.ValidatorMap`
         used to validate the request
-    :param validation_context: a context manager for wrapping validation
-        errors
     :returns: a :class:`dict` of request data for each parameter in the swagger
         spec
+    :raises: RequestValidationError when the request is not valid for the
+        swagger spec
     """
     request_data = {}
     validation_pairs = []
@@ -342,8 +343,7 @@ def handle_request(request, validator_map, validation_context, **kwargs):
         validation_pairs.append((validator_map.body, request.body))
         request_data[param_name] = request.body
 
-    with validation_context(request):
-        validate_request(validation_pairs)
+    validate_request(validation_pairs)
 
     return request_data
 
@@ -467,7 +467,7 @@ def validation_error(exc_class):
                 # This will alter our stack trace slightly, but Pyramid knows
                 # how to render it. And the real value is in the message
                 # anyway.
-                e = exc_class(str(exc))
+                e = exc_class(str(exc), child=exc)
                 e._traceback = sys.exc_info()[2]
                 raise e
 
@@ -568,11 +568,8 @@ def swaggerize_request(request, op, **kwargs):
 
     :type request: :class:`pyramid.request.Request`
     :type op: :class:`bravado_core.operation.Operation`
-    :type validation_context: context manager
     """
-    validation_context = kwargs['validation_context']
-    with validation_context(request):
-        request_data = unmarshal_request(request, op)
+    request_data = unmarshal_request(request, op)
     return request_data
 
 


Test patch:
diff --git a/tests/acceptance/request_test.py b/tests/acceptance/request_test.py
index 91735c7..d8503ea 100644
--- a/tests/acceptance/request_test.py
+++ b/tests/acceptance/request_test.py
@@ -7,6 +7,8 @@ from _pytest.fixtures import FixtureRequest
 from mock import Mock
 from pyramid.httpexceptions import exception_response
 
+from pyramid_swagger import exceptions
+
 
 # Parameterize pyramid_swagger.swagger_versions
 @pytest.fixture(params=[['1.2'], ['2.0'], ['1.2', '2.0']])
@@ -29,8 +31,15 @@ def test_app(request, **overrides):
 def validation_context(request, response=None):
     try:
         yield
-    except Exception:
+    except (
+        exceptions.RequestValidationError,
+        exceptions.ResponseValidationError,
+        exceptions.PathNotFoundError,
+    ) as e:
         raise exception_response(206)
+        assert e.child is not None
+    except Exception:
+        raise exception_response(400)
 
 
 validation_ctx_path = 'tests.acceptance.request_test.validation_context'
diff --git a/tests/tween_test.py b/tests/tween_test.py
index 8001d80..08aa2d9 100644
--- a/tests/tween_test.py
+++ b/tests/tween_test.py
@@ -24,7 +24,6 @@ from pyramid_swagger.tween import get_op_for_request
 from pyramid_swagger.tween import get_swagger_objects
 from pyramid_swagger.tween import get_swagger_versions
 from pyramid_swagger.tween import handle_request
-from pyramid_swagger.tween import noop_context
 from pyramid_swagger.tween import prepare_body
 from pyramid_swagger.tween import PyramidSwaggerRequest
 from pyramid_swagger.tween import PyramidSwaggerResponse
@@ -202,7 +201,7 @@ def test_handle_request_returns_request_data():
         'bar': {'more': 'foo'},
     }
 
-    request_data = handle_request(mock_request, validator_map, noop_context,)
+    request_data = handle_request(mock_request, validator_map)
     assert request_data == expected
 
 
@@ -267,6 +266,19 @@ def test_validaton_error_decorator_transforms_SwaggerMappingError():
     assert 'kaboom' in str(excinfo.value)
 
 
+def test_validation_error_includes_child():
+
+    @validation_error(RequestValidationError)
+    def foo():
+        raise SwaggerMappingError('kaboom')
+
+    try:
+        foo()
+    except RequestValidationError as e:
+        assert isinstance(e.child, SwaggerMappingError)
+        assert 'kaboom' in str(e)
+
+
 @pytest.fixture
 def registry():
     config = {