Skip to content

Commit

Permalink
Merge pull request #415 from dairiki/feature.schema_node
Browse files Browse the repository at this point in the history
Support passing colander schema nodes (instances)
  • Loading branch information
leplatrem committed Oct 27, 2016
2 parents 94b47ef + 42fdaae commit ba6b49a
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 21 deletions.
13 changes: 13 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ CHANGELOG
2.0.3 (unreleased)
==================

**Enhancements**

- ``Cornice.validators.colander_validator`` and
``cornice.validators.colander_body_validator`` now accept colander
schema node instances. Previously only schema classes were
accepted. For some discussion see #412.

**Deprecations**

- Passing schema classes to ``Cornice.validators.colander_validator`` and
``cornice.validators.colander_body_validator`` is now deprecated.
(See above.)

**Bug fixes**

- To maintain consistency with cornice 1.2 as to the semantics of
Expand Down
24 changes: 19 additions & 5 deletions cornice/validators/_colander.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
# 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 inspect
import warnings


def body_validator(request, schema=None, deserializer=None, **kwargs):
"""
Expand All @@ -17,20 +20,20 @@ def body_validator(request, schema=None, deserializer=None, **kwargs):
:param request: Current request
:type request: :class:`~pyramid:pyramid.request.Request`
:param schema: The Colander schema class
:param schema: The Colander schema
:param deserializer: Optional deserializer, defaults to
:func:`cornice.validators.extract_cstruct`
"""
import colander

if schema is not None:
class RequestSchema(colander.MappingSchema):
body = schema()
body = _ensure_instantiated(schema)

def deserialize(self, cstruct=colander.null):
appstruct = super(RequestSchema, self).deserialize(cstruct)
return appstruct['body']
schema = RequestSchema
schema = RequestSchema()
return validator(request, schema, deserializer, **kwargs)


Expand All @@ -49,7 +52,7 @@ def validator(request, schema=None, deserializer=None, **kwargs):
:param request: Current request
:type request: :class:`~pyramid:pyramid.request.Request`
:param schema: The Colander schema class
:param schema: The Colander schema
:param deserializer: Optional deserializer, defaults to
:func:`cornice.validators.extract_cstruct`
"""
Expand All @@ -62,7 +65,7 @@ def validator(request, schema=None, deserializer=None, **kwargs):
if schema is None:
raise TypeError('This validator cannot work without a schema')

schema = schema()
schema = _ensure_instantiated(schema)
cstruct = deserializer(request)
try:
deserialized = schema.deserialize(cstruct)
Expand All @@ -81,3 +84,14 @@ def validator(request, schema=None, deserializer=None, **kwargs):
field = prefixed[1]

request.errors.add(location, field, error_dict[name])


def _ensure_instantiated(schema):
if inspect.isclass(schema):
warnings.warn(
"Setting schema to a class is deprecated. "
" Set schema to an instance instead.",
DeprecationWarning,
stacklevel=2)
schema = schema()
return schema
6 changes: 3 additions & 3 deletions docs/source/schema.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ To describe a schema, using Colander and Cornice, here is how you can do:
class SignupSchema(colander.MappingSchema):
username = colander.SchemaNode(colander.String())
@signup.post(schema=SignupSchema, validators=(colander_body_validator,))
@signup.post(schema=SignupSchema(), validators=(colander_body_validator,))
def signup_post(request):
username = request.validated['username']
return {'success': True}
Expand Down Expand Up @@ -120,7 +120,7 @@ The ``request.validated`` hences reflects this additional level.
signup = cornice.Service()
@signup.post(schema=SignupSchema, validators=(colander_validator,))
@signup.post(schema=SignupSchema(), validators=(colander_validator,))
def signup_post(request):
username = request.validated['body']['username']
referrer = request.validated['querystring']['referrer']
Expand Down Expand Up @@ -161,7 +161,7 @@ The general pattern in this case is:
return extract_data_somehow(request)
@service.post(schema=MySchema,
@service.post(schema=MySchema(),
deserializer=my_deserializer,
validators=(colander_body_validator,))
def post(request):
Expand Down
6 changes: 3 additions & 3 deletions docs/source/upgrading.rst
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ Now:
class SignupSchema(colander.MappingSchema):
username = colander.SchemaNode(colander.String())
@signup.post(schema=SignupSchema, validators=(colander_body_validator,))
@signup.post(schema=SignupSchema(), validators=(colander_body_validator,))
def signup_postt(request):
username = request.validated['username']
return {'success': True}
Expand Down Expand Up @@ -145,7 +145,7 @@ Now:
signup = cornice.Service()
@signup.post(schema=SignupSchema, validators=(colander_validator,))
@signup.post(schema=SignupSchema(), validators=(colander_validator,))
def signup_post(request):
username = request.validated['body']['username']
referrer = request.validated['querystring']['referrer']
Expand Down Expand Up @@ -207,7 +207,7 @@ Deserializers are still defined via the same API:
return dict(zip(['foo', 'bar', 'yeah'], values))
request.errors.add(location='body', description='Unsupported content')
@myservice.post(schema=FooBarSchema,
@myservice.post(schema=FooBarSchema(),
deserializer=dummy_deserializer,
validators=(my_validator,))
Expand Down
36 changes: 31 additions & 5 deletions tests/test_colander.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# 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 warnings

from cornice.errors import Errors
from cornice.validators._colander import validator
Expand Down Expand Up @@ -66,11 +67,36 @@ def __init__(self, body, json_body, get, method='GET'):
return dummy_request

class TestSchemas(TestCase):
def test_body_contains_fields(self):
def test_validation(self):
body = {'bar': '1',
'baz': 2,
'baz': '2',
'foo': 'yeah'}
headers = {'x-foo': 'version_a'}
request = get_mock_request(body)
validator(request, schema=RequestSchema())
self.assertEqual(len(request.errors), 0)
self.assertEqual(request.validated['body'], {
'foo': 'yeah',
'bar': '1',
'baz': 2,
})

dummy_request = get_mock_request(body, headers=headers)
validator(dummy_request, schema=RequestSchema)
def test_validation_failure(self):
body = {'bar': '1',
'baz': 'two',
'foo': 'yeah'}
request = get_mock_request(body)
validator(request, schema=RequestSchema())
self.assertEqual(len(request.errors), 1)
self.assertEqual(request.validated, {})
error = request.errors[0]
self.assertEqual(error['location'], 'body')
self.assertEqual(error['name'], 'baz')

def test_schema_class_deprecated(self):
body = {}
request = get_mock_request(body)
with warnings.catch_warnings(record=True) as w:
warnings.resetwarnings()
validator(request, schema=RequestSchema)
self.assertEqual(len(w), 1)
self.assertIs(w[0].category, DeprecationWarning)
10 changes: 5 additions & 5 deletions tests/validationapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ def post7(request):
class SignupSchema(MappingSchema):
username = SchemaNode(String())

@signup.post(schema=SignupSchema, validators=(colander_body_validator,))
@signup.post(schema=SignupSchema(), validators=(colander_body_validator,))
def signup_post(request):
return request.validated

Expand Down Expand Up @@ -206,7 +206,7 @@ def deserialize(self, cstruct):

foobar = Service(name="foobar", path="/foobar")

@foobar.post(schema=RequestSchema, validators=(colander_validator,))
@foobar.post(schema=RequestSchema(), validators=(colander_validator,))
def foobar_post(request):
return {"test": "succeeded"}

Expand All @@ -226,7 +226,7 @@ class QSSchema(MappingSchema):

foobaz = Service(name="foobaz", path="/foobaz")

@foobaz.get(schema=QSSchema, validators=(colander_validator,))
@foobaz.get(schema=QSSchema(), validators=(colander_validator,))
def foobaz_get(request):
return {"field": request.validated['querystring']['field']}

Expand All @@ -250,7 +250,7 @@ def deserialize(self, cstruct=null):

email_service = Service(name='newsletter', path='/newsletter')

@email_service.post(schema=NewsletterPayload,
@email_service.post(schema=NewsletterPayload(),
validators=(colander_validator,))
def newsletter(request):
return request.validated
Expand All @@ -263,7 +263,7 @@ class ItemSchema(MappingSchema):

item_service = Service(name='item', path='/item/{item_id}')

@item_service.get(schema=ItemSchema,
@item_service.get(schema=ItemSchema(),
validators=(colander_validator,))
def item(request):
return request.validated['path']
Expand Down

0 comments on commit ba6b49a

Please sign in to comment.