Skip to content
Browse files

merged from remote master

  • Loading branch information...
2 parents 6e060a5 + 5a9b0d7 commit 5599bfff29d429e0ca269a236e07b769c2df4a6e @vincentfretin vincentfretin committed Apr 1, 2012
View
17 CHANGES.txt
@@ -1,8 +1,23 @@
-0.7 - ?
+0.8 - ?
=======
+?
+
+
+0.7 - 2012-03-12
+================
+
- update license to MPL 2.0.
- renamed cornice.schemas to cornice.errors
+- Added `get_view_wrapper` method to Service class to support subclasses
+ wrapping the view callables w/ decorators
+- added buildout support
+- added class-based views and the resource decorator
+- make sure we use Pyramid's exceptions. Not Webob's.
+- added filters support
+- added schema support
+- added json xsrf support
+- now errors status can be different from 400.
0.6 - 2011-12-21
View
2 cornice/resource.py
@@ -42,7 +42,7 @@ def wrapper(klass):
**service_args)
# initialize views
- for verb in ('get', 'post', 'put', 'delete'):
+ for verb in ('get', 'post', 'put', 'delete', 'options'):
view_attr = prefix + verb
meth = getattr(klass, view_attr, None)
if meth is not None:
View
24 cornice/service.py
@@ -134,6 +134,21 @@ def put(self, **kw):
def delete(self, **kw):
return self.api(request_method='DELETE', **kw)
+ def options(self, **kw):
+ return self.api(request_method='OPTIONS', **kw)
+
+ def get_view_wrapper(self, kw):
+ """
+ Overload this method if you would like to wrap the API function
+ function just before it is registered as a view callable. This will be
+ called with the api() call kwargs, it should return a callable which
+ accepts a single function and returns a single function. Right before
+ view registration, this will be called w/ the function to register, and
+ the return value will be registered in its stead. By default this
+ simply returns the same function it was passed.
+ """
+ return lambda func: func
+
# the actual decorator
def api(self, **kw):
"""Decorates a function to make it a service.
@@ -142,13 +157,17 @@ def api(self, **kw):
delete are aliases to this one, specifying the "request_method"
argument for convenience.
- ;param request_method: the request method. Should be one of GET, POST,
+ :param request_method: the request method. Should be one of GET, POST,
PUT, DELETE, OPTIONS, HEAD, TRACE or CONNECT
+ :param decorators: A sequence of decorators which should be applied
+ to the view callable before it's returned. Will be
+ applied in order received, i.e. the last decorator
+ in the sequence will be the outermost wrapper.
All the constructor options, minus name and path, can be overwritten in
here.
"""
-
+ view_wrapper = self.get_view_wrapper(kw)
method = kw.get('request_method', 'GET') # default is GET
api_kw = self.kw.copy()
api_kw.update(kw)
@@ -236,6 +255,7 @@ def view(request):
config.add_view(view=view, route_name=self.route_name,
**view_kw)
+ func = view_wrapper(func)
info = venusian.attach(func, callback, category='pyramid')
if info.scope == 'class':
View
1 cornice/sphinxext.py
@@ -47,6 +47,7 @@ def trim(docstring):
from sphinx.locale import l_
from sphinx.util.docfields import Field, GroupedField, TypedField
+
class ServiceDirective(Directive):
""" Service directive.
View
33 cornice/tests/test_service.py
@@ -54,3 +54,36 @@ def bah(req):
pass
self.assertEqual(msg, str(w[-1].message))
+
+
+class WrapperService(Service):
+ def get_view_wrapper(self, kw):
+ def upper_wrapper(func):
+ def upperizer(*args, **kwargs):
+ result = func(*args, **kwargs)
+ return result.upper()
+ return upperizer
+ return upper_wrapper
+
+
+wrapper_service = WrapperService(name='wrapperservice', path='/wrapperservice')
+
+
+@wrapper_service.get()
+def return_foo(request):
+ return 'foo'
+
+
+class TestServiceWithWrapper(unittest.TestCase):
+ def setUp(self):
+ self.config = testing.setUp()
+ self.config.include("cornice")
+ self.config.scan("cornice.tests.test_service")
+ self.app = TestApp(CatchErrors(self.config.make_wsgi_app()))
+
+ def tearDown(self):
+ testing.tearDown()
+
+ def test_wrapped(self):
+ result = self.app.get('/wrapperservice')
+ self.assertEqual(result.json, 'FOO')
View
225 cornice/tests/test_service_description.py
@@ -44,111 +44,122 @@
from cornice.tests import CatchErrors
from cornice.schemas import CorniceSchema
-from colander import Invalid, MappingSchema, SchemaNode, String
+try:
+ from colander import Invalid, MappingSchema, SchemaNode, String
+ COLANDER = True
+except ImportError:
+ COLANDER = False
+
+if COLANDER:
+ foobar = Service(name="foobar", path="/foobar")
+
+ def validate_bar(node, value):
+ if value != 'open':
+ raise Invalid(node, "The bar is not open.")
+
+ class FooBarSchema(MappingSchema):
+ # foo and bar are required, baz is optional
+ foo = SchemaNode(String(), location="body", type='str')
+ bar = SchemaNode(String(), location="body", type='str', validator=validate_bar)
+ baz = SchemaNode(String(), location="body", type='str', missing=None)
+ yeah = SchemaNode(String(), location="querystring", type='str')
+
+ class SchemaFromQuerystring(MappingSchema):
+ yeah = SchemaNode(String(), location="querystring", type='str')
+
+ @foobar.post(schema=FooBarSchema)
+ def foobar_post(request):
+ return {"test": "succeeded", 'baz': request.validated['baz']}
+
+ @foobar.get(schema=SchemaFromQuerystring)
+ def foobar_get(request):
+ return {"test": "succeeded"}
+
+ class TestServiceDescription(unittest.TestCase):
+
+ def setUp(self):
+ self.config = testing.setUp()
+ self.config.include("cornice")
+ self.config.scan("cornice.tests.test_service_description")
+ self.app = TestApp(CatchErrors(self.config.make_wsgi_app()))
+
+ def tearDown(self):
+ testing.tearDown()
+
+ def test_get_from_colander(self):
+ schema = CorniceSchema.from_colander(FooBarSchema)
+ attrs = schema.as_dict()
+ self.assertEqual(len(attrs), 4)
+
+ def test_description_attached(self):
+ # foobar should contain a schema argument containing the cornice
+ # schema object, so it can be introspected if needed
+ self.assertTrue('POST' in foobar.schemas)
+
+ def test_schema_validation(self):
+ # using a colander schema for the service should automatically
+ # validate the request calls. Let's make some of them here.
+ resp = self.app.post('/foobar', status=400)
+ self.assertEqual(resp.json['status'], 'error')
+
+ errors = resp.json['errors']
+ # we should at have 1 missing value in the QS...
+ self.assertEqual(1, len([e for e in errors
+ if e['location'] == "querystring"]))
+
+ # ... and 2 in the body (a json error as well)
+ self.assertEqual(2, len([e for e in errors
+ if e['location'] == "body"]))
+
+ # let's do the same request, but with information in the
+ # querystring
+ resp = self.app.post('/foobar?yeah=test', status=400)
+
+ # we should at have no missing value in the QS
+ self.assertEqual(0, len([e for e in resp.json['errors']
+ if e['location'] == "querystring"]))
+
+ # and if we add the required values in the body of the post,
+ # then we should be good
+ data = {'foo': 'yeah', 'bar': 'open'}
+ resp = self.app.post('/foobar?yeah=test', params=json.dumps(data),
+ status=200)
+
+ self.assertEqual(resp.json, {u'baz': None, "test": "succeeded"})
+
+ def test_schema_validation2(self):
+ resp = self.app.get('/foobar?yeah=test', status=200)
+ self.assertEqual(resp.json, {"test": "succeeded"})
+
+ def test_bar_validator(self):
+ # test validator on bar attribute
+ data = {'foo': 'yeah', 'bar': 'closed'}
+ resp = self.app.post('/foobar?yeah=test', params=json.dumps(data),
+ status=400)
+
+ self.assertEqual(resp.json, {
+ u'errors': [{u'description': u'The bar is not open.',
+ u'location': u'body',
+ u'name': u'bar'}],
+ u'status': u'error'})
+
+ def test_foo_required(self):
+ # test required attribute
+ data = {'bar': 'open'}
+ resp = self.app.post('/foobar?yeah=test', params=json.dumps(data),
+ status=400)
+
+ self.assertEqual(resp.json, {
+ u'errors': [{u'description': u'foo is missing',
+ u'location': u'body',
+ u'name': u'foo'}],
+ u'status': u'error'})
+
+ def test_default_baz_value(self):
+ # test required attribute
+ data = {'foo': 'yeah', 'bar': 'open'}
+ resp = self.app.post('/foobar?yeah=test', params=json.dumps(data),
+ status=200)
+
+ self.assertEqual(resp.json, {u'baz': None, "test": "succeeded"})
-
-foobar = Service(name="foobar", path="/foobar")
-
-
-def validate_bar(node, value):
- if value != 'open':
- raise Invalid(node, "The bar is not open.")
-
-
-class FooBarSchema(MappingSchema):
- # foo and bar are required, baz is optional
- foo = SchemaNode(String(), location="body", type='str')
- bar = SchemaNode(String(), location="body", type='str', validator=validate_bar)
- baz = SchemaNode(String(), location="body", type='str', missing=None)
- yeah = SchemaNode(String(), location="querystring", type='str')
-
-
-@foobar.post(schema=FooBarSchema)
-def foobar_post(request):
- return {"test": "succeeded", 'baz': request.validated['baz']}
-
-
-class TestServiceDescription(unittest.TestCase):
-
- def setUp(self):
- self.config = testing.setUp()
- self.config.include("cornice")
- self.config.scan("cornice.tests.test_service_description")
- self.app = TestApp(CatchErrors(self.config.make_wsgi_app()))
-
- def tearDown(self):
- testing.tearDown()
-
- def test_get_from_colander(self):
- schema = CorniceSchema.from_colander(FooBarSchema)
- attrs = schema.as_dict()
- self.assertEqual(len(attrs), 4)
-
- def test_description_attached(self):
- # foobar should contain a schema argument containing the cornice
- # schema object, so it can be introspected if needed
- self.assertTrue('POST' in foobar.schemas)
-
- def test_schema_validation(self):
- # using a colander schema for the service should automatically validate
- # the request calls. Let's make some of them here
-
- resp = self.app.post('/foobar', status=400)
- self.assertEqual(resp.json['status'], 'error')
-
- errors = resp.json['errors']
- # we should at have 1 missing value in the QS...
- self.assertEqual(1, len([e for e in errors
- if e['location'] == "querystring"]))
-
- # ... and 3 in the body (a json error as well)
- self.assertEqual(3, len([e for e in errors
- if e['location'] == "body"]))
-
-
- # let's do the same request, but with information in the querystring
- resp = self.app.post('/foobar?yeah=test', status=400)
-
- # we should at have no missing value in the QS
- self.assertEqual(0, len([e for e in resp.json['errors']
- if e['location'] == "querystring"]))
-
- # and if we add the required values in the body of the post, then we
- # should be good
- data = {'foo': 'yeah', 'bar': 'open'}
- resp = self.app.post('/foobar?yeah=test', params=json.dumps(data),
- status=200)
-
- self.assertEqual(resp.json, {u'baz': None, "test": "succeeded"})
-
- def test_bar_validator(self):
- # test validator on bar attribute
- data = {'foo': 'yeah', 'bar': 'closed'}
- resp = self.app.post('/foobar?yeah=test', params=json.dumps(data),
- status=400)
-
- self.assertEqual(resp.json, {
- u'errors': [{u'description': u'The bar is not open.',
- u'location': u'body',
- u'name': u'bar'}],
- u'status': u'error'})
-
- def test_foo_required(self):
- # test required attribute
- data = {'bar': 'open'}
- resp = self.app.post('/foobar?yeah=test', params=json.dumps(data),
- status=400)
-
- self.assertEqual(resp.json, {
- u'errors': [{u'description': u'foo is missing',
- u'location': u'body',
- u'name': u'foo'}],
- u'status': u'error'})
-
- def test_default_baz_value(self):
- # test required attribute
- data = {'foo': 'yeah', 'bar': 'open'}
- resp = self.app.post('/foobar?yeah=test', params=json.dumps(data),
- status=200)
-
- self.assertEqual(resp.json, {u'baz': None, "test": "succeeded"})
View
44 cornice/tests/validationapp.py
@@ -5,7 +5,6 @@
from cornice import Service
from cornice.tests import CatchErrors
-from colander import Invalid, MappingSchema, SchemaNode, String
import json
@@ -83,26 +82,29 @@ def _filter(response):
def get4(request):
return "unfiltered" # should be overwritten on GET
-
-def validate_bar(node, value):
- if value != 'open':
- raise Invalid(node, "The bar is not open.")
-
-
-class FooBarSchema(MappingSchema):
- # foo and bar are required, baz is optional
- foo = SchemaNode(String(), location="body", type='str')
- bar = SchemaNode(String(), location="body", type='str', validator=validate_bar)
- baz = SchemaNode(String(), location="body", type='str', missing=None)
- yeah = SchemaNode(String(), location="querystring", type='str')
-
-
-foobar = Service(name="foobar", path="/foobar")
-
-
-@foobar.post(schema=FooBarSchema)
-def foobar_post(request):
- return {"test": "succeeded"}
+try:
+ from colander import Invalid, MappingSchema, SchemaNode, String
+ COLANDER = True
+except ImportError:
+ COLANDER = False
+
+if COLANDER:
+ def validate_bar(node, value):
+ if value != 'open':
+ raise Invalid(node, "The bar is not open.")
+
+ class FooBarSchema(MappingSchema):
+ # foo and bar are required, baz is optional
+ foo = SchemaNode(String(), location="body", type='str')
+ bar = SchemaNode(String(), location="body", type='str', validator=validate_bar)
+ baz = SchemaNode(String(), location="body", type='str', missing=None)
+ yeah = SchemaNode(String(), location="querystring", type='str')
+
+ foobar = Service(name="foobar", path="/foobar")
+
+ @foobar.post(schema=FooBarSchema)
+ def foobar_post(request):
+ return {"test": "succeeded"}
def includeme(config):
View
11 cornice/util.py
@@ -108,10 +108,13 @@ def extract_request_data(request):
them as a list of (querystring, headers, body, path)
"""
# XXX In the body, we're only handling JSON for now.
- try:
- body = json.loads(request.body)
- except ValueError, e:
- request.errors.add('body', None, e.message)
+ if request.body:
+ try:
+ body = json.loads(request.body)
+ except ValueError, e:
+ request.errors.add('body', None, e.message)
+ body = {}
+ else:
body = {}
return request.GET, request.headers, body, request.matchdict
View
2 setup.py
@@ -27,7 +27,7 @@
setup(name='cornice',
- version='0.7',
+ version='0.8',
description='Define Web Services in Pyramid.',
long_description=README + '\n\n' + CHANGES,
classifiers=[

0 comments on commit 5599bff

Please sign in to comment.
Something went wrong with that request. Please try again.