Skip to content

Commit

Permalink
Merge pull request #335 from delijati/imperative
Browse files Browse the repository at this point in the history
adding abillity to define services imperative
  • Loading branch information
almet committed Oct 11, 2015
2 parents 16ce0e6 + f641c61 commit 7f3a9b9
Show file tree
Hide file tree
Showing 11 changed files with 535 additions and 80 deletions.
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Cornice:
* David Charboneau
* Gael Pasgrimaud <gael@gawel.org>
* Janek Hiis <janek@utopic.me>
* Josip Delic <delicj@delijati.net>
* Lorenzo Gil Sanchez <lorenzo.gil.sanchez@gmail.com>
* Marc Abramowitz <marca@surveymonkey.com>
* Marcin Lulek <info@webreactor.eu>
Expand Down
2 changes: 2 additions & 0 deletions cornice/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
register_service_views,
handle_exceptions,
add_deserializer,
register_resource_views,
)
from cornice.util import ContentTypePredicate

Expand Down Expand Up @@ -42,6 +43,7 @@ def includeme(config):

# config.add_directive('add_apidoc', add_apidoc)
config.add_directive('add_cornice_service', register_service_views)
config.add_directive('add_cornice_resource', register_resource_views)
config.add_directive('add_cornice_deserializer', add_deserializer)
config.add_subscriber(add_renderer_globals, BeforeRender)
config.add_subscriber(wrap_request, NewRequest)
Expand Down
11 changes: 9 additions & 2 deletions cornice/ext/sphinxext.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,15 @@ class ServiceDirective(Directive):
.. cornice-autodoc::
:modules: your.module
:app: load app to work with imperative add_view call.
:services: name1, name2
:service: name1 # no need to specify both services and service.
:ignore: a comma separated list of services names to ignore
"""
has_content = True
option_spec = {'modules': convert_to_list_required,
option_spec = {'modules': convert_to_list,
'app': directives.unchanged,
'service': directives.unchanged,
'services': convert_to_list,
'ignore': convert_to_list}
Expand All @@ -71,8 +73,13 @@ def run(self):
# directive multiple times
clear_services()

app_name = self.options.get('app')
if app_name:
app = import_module(app_name)
app.main({})

# import the modules, which will populate the SERVICES variable.
for module in self.options.get('modules'):
for module in self.options.get('modules', []):
if module in MODULES:
reload(MODULES[module])
else:
Expand Down
14 changes: 14 additions & 0 deletions cornice/pyramidhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,3 +335,17 @@ def callback():
registry.cornice_deserializers[content_type] = deserializer

config.action(content_type, callable=callback)


def register_resource_views(config, resource):
"""Register a resource and it's views.
:param config:
The pyramid configuration object that will be populated.
:param resource:
The resource class containing the definitions
"""
services = resource._services

for service in services.values():
config.add_cornice_service(service)
213 changes: 141 additions & 72 deletions cornice/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,96 +11,165 @@
VENUSIAN = False


def resource(depth=1, **kw):
def resource(depth=2, **kw):
"""Class decorator to declare resources.
All the methods of this class named by the name of HTTP resources
will be used as such. You can also prefix them by "collection_" and they
will be treated as HTTP methods for the given collection path
(collection_path), if any.
:param depth:
Witch frame should be looked in default 2.
:param kw:
Keyword arguments configuring the resource.
Here is an example::
@resource(collection_path='/users', path='/users/{id}')
"""
def wrapper(klass):
services = {}

if 'collection_path' in kw:
if kw['collection_path'] == kw['path']:
msg = "Warning: collection_path and path are not distinct."
warnings.warn(msg)

prefixes = ('', 'collection_')
else:
prefixes = ('',)

for prefix in prefixes:

# get clean view arguments
service_args = {}
for k in list(kw):
if k.startswith('collection_'):
if prefix == 'collection_':
service_args[k[len(prefix):]] = kw[k]
elif k not in service_args:
service_args[k] = kw[k]

if prefix == 'collection_' and service_args.get('collection_acl'):
service_args['acl'] = service_args['collection_acl']

# create service
service_name = (service_args.pop('name', None) or
klass.__name__.lower())
service_name = prefix + service_name
service = services[service_name] = Service(name=service_name,
depth=2, **service_args)

# initialize views
for verb in ('get', 'post', 'put', 'delete', 'options', 'patch'):

view_attr = prefix + verb
meth = getattr(klass, view_attr, None)

if meth is not None:
# if the method has a __views__ arguments, then it had
# been decorated by a @view decorator. get back the name of
# the decorated method so we can register it properly
views = getattr(meth, '__views__', [])
if views:
for view_args in views:
service.add_view(verb, view_attr, klass=klass,
**view_args)
else:
service.add_view(verb, view_attr, klass=klass)

setattr(klass, '_services', services)

if VENUSIAN:
def callback(context, name, ob):
# get the callbacks registred by the inner services
# and call them from here when the @resource classes are being
# scanned by venusian.
for service in services.values():
config = context.config.with_package(info.module)
config.add_cornice_service(service)

info = venusian.attach(klass, callback, category='pyramid',
depth=depth)
return klass
return add_resource(klass, depth, **kw)
return wrapper


def add_resource(klass, depth=1, **kw):
"""Function to declare resources of a Class.
All the methods of this class named by the name of HTTP resources
will be used as such. You can also prefix them by "collection_" and they
will be treated as HTTP methods for the given collection path
(collection_path), if any.
:param klass:
The class (resource) on witch to register the service.
:param depth:
Witch frame should be looked in default 2.
:param kw:
Keyword arguments configuring the resource.
Here is an example::
class User(object):
pass
add_resource(User, collection_path='/users', path='/users/{id}')
"""

services = {}

if 'collection_path' in kw:
if kw['collection_path'] == kw['path']:
msg = "Warning: collection_path and path are not distinct."
warnings.warn(msg)

prefixes = ('', 'collection_')
else:
prefixes = ('',)

for prefix in prefixes:

# get clean view arguments
service_args = {}
for k in list(kw):
if k.startswith('collection_'):
if prefix == 'collection_':
service_args[k[len(prefix):]] = kw[k]
elif k not in service_args:
service_args[k] = kw[k]

if prefix == 'collection_' and service_args.get('collection_acl'):
service_args['acl'] = service_args['collection_acl']

# create service
service_name = (service_args.pop('name', None) or
klass.__name__.lower())
service_name = prefix + service_name
service = services[service_name] = Service(name=service_name,
depth=2, **service_args)

# initialize views
for verb in ('get', 'post', 'put', 'delete', 'options', 'patch'):

view_attr = prefix + verb
meth = getattr(klass, view_attr, None)

if meth is not None:
# if the method has a __views__ arguments, then it had
# been decorated by a @view decorator. get back the name of
# the decorated method so we can register it properly
views = getattr(meth, '__views__', [])
if views:
for view_args in views:
service.add_view(verb, view_attr, klass=klass,
**view_args)
else:
service.add_view(verb, view_attr, klass=klass)

setattr(klass, '_services', services)

if VENUSIAN:
def callback(context, name, ob):
# get the callbacks registred by the inner services
# and call them from here when the @resource classes are being
# scanned by venusian.
for service in services.values():
config = context.config.with_package(info.module)
config.add_cornice_service(service)

info = venusian.attach(klass, callback, category='pyramid',
depth=depth)
return klass


def view(**kw):
"""Method decorator to store view arguments when defining a resource with
the @resource class decorator
:param kw:
Keyword arguments configuring the view.
"""
def wrapper(func):
# store view argument to use them later in @resource
views = getattr(func, '__views__', None)
if views is None:
views = []
setattr(func, '__views__', views)
views.append(kw)
return func
return add_view(func, **kw)
return wrapper


def add_view(func, **kw):
"""Method to store view arguments when defining a resource with
the add_resource class method
:param func:
The func to hook to
:param kw:
Keyword arguments configuring the view.
Example:
class User(object):
def __init__(self, request):
self.request = request
def collection_get(self):
return {'users': _USERS.keys()}
def get(self):
return _USERS.get(int(self.request.matchdict['id']))
add_view(User.get, renderer='json')
add_resource(User, collection_path='/users', path='/users/{id}')
"""
# XXX needed in py2 to set on instancemethod
if hasattr(func, '__func__'):
func = func.__func__
# store view argument to use them later in @resource
views = getattr(func, '__views__', None)
if views is None:
views = []
setattr(func, '__views__', views)
views.append(kw)
return func
14 changes: 14 additions & 0 deletions cornice/tests/ext/dummy/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from pyramid.config import Configurator


def main(global_config, **settings):
config = Configurator(settings=settings)
# adds cornice
config.include("cornice")
# adds application-specific views
config.include("cornice.tests.ext.dummy.views.includeme")
return config.make_wsgi_app()


if __name__ == '__main__':
main({})
35 changes: 35 additions & 0 deletions cornice/tests/ext/dummy/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from collections import defaultdict
from cornice import Service
from cornice import resource

_USERS = defaultdict(dict)


class ThingImp(object):

def __init__(self, request, context=None):
self.request = request
self.context = context

def collection_get(self):
"""returns yay"""
return 'yay'


def get_info(request):
"returns the user data"
username = request.matchdict['username']
return _USERS[username]


def includeme(config):
# FIXME this should also work in includeme
user_info = Service(name='users', path='/{username}/info')
user_info.add_view('get', get_info)
config.add_cornice_service(user_info)

resource.add_view(ThingImp.collection_get, permission='read')
thing_resource = resource.add_resource(
ThingImp, collection_path='/thing', path='/thing/{id}',
name='thing_service')
config.add_cornice_resource(thing_resource)
16 changes: 14 additions & 2 deletions cornice/tests/ext/test_sphinxext.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,17 @@ def test_module_reload(self):
directive = ServiceDirective(
'test', [], {}, [], 1, 1, 'test', mock.Mock(), 1)
directive.options['modules'] = ['cornice']
directive.run()
directive.run()
ret = directive.run()
self.assertEqual(ret, [])

def test_dummy(self):
param = mock.Mock()
param.document.settings.env.new_serialno.return_value = 1
directive = ServiceDirective(
'test', [], {}, [], 1, 1, 'test', param, 1)
directive.options['app'] = 'cornice.tests.ext.dummy'
directive.options['services'] = ['users', "thing_service"]
ret = directive.run()
self.assertEqual(len(ret), 2)
self.assertTrue('Users service at' in str(ret[0]))
self.assertTrue('Thing_Service service at ' in str(ret[1]))

0 comments on commit 7f3a9b9

Please sign in to comment.