Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

add_resource #1

Merged
merged 10 commits into from

3 participants

@yeeland

I've never done this before, but I thought I'd give it a try.

I tried emulating map.resource from Routes.

@mcdonc
Owner

Thanks for the submission! You might want to poke Ben Bangert, owner of this package on the maillist or IRC, I'mnot sure he's keeping an eye on github pull requests.

@yeeland

I pinged Ben yesterday. It sounds like his plate his pretty full but he will be having a close look at things as soon as he can.

@bbangert bbangert merged commit 9402837 into Pylons:master
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Apr 6, 2011
  1. first pass at add_resource implementation

    Yeeland Chen authored
  2. unit tests stubbed out with 100% coverage. TODO: check configurator o…

    Yeeland Chen authored
    …bjects for correct setup.
Commits on Apr 7, 2011
  1. unit testing for URL generation

    Yeeland Chen authored
  2. moved self.config.begin()

    Yeeland Chen authored
  3. read should be show

    Yeeland Chen authored
  4. renamed testcase to resource generation

    Yeeland Chen authored
  5. corrected example documentation

    Yeeland Chen authored
Commits on Apr 8, 2011
  1. unit tests to make sure routing was set up correctly

    Yeeland Chen authored
Commits on Apr 10, 2011
Commits on Apr 18, 2011
  1. return and check for strings instead of json

    Yeeland Chen authored
This page is out of date. Refresh to see the latest.
Showing with 528 additions and 1 deletion.
  1. +287 −1 pyramid_routehelper/__init__.py
  2. +241 −0 pyramid_routehelper/tests.py
View
288 pyramid_routehelper/__init__.py
@@ -1 +1,287 @@
-#
+from pyramid.config import ConfigurationError
+import inspect
+
+__all__ = ['includeme', 'add_resource', 'action']
+
+def includeme(config):
+ config.add_directive('add_resource', add_resource)
+
+def strip_slashes(name):
+ """Remove slashes from the beginning and end of a part/URL."""
+ if name.startswith('/'):
+ name = name[1:]
+ if name.endswith('/'):
+ name = name[:-1]
+ return name
+
+class action(object):
+ """Decorate a method for registration by
+ :func:`~pyramid_routehelper.add_resource`.
+
+ Keyword arguments are identical to :class:`~pyramid.view.view_config`, with
+ the exception to how the ``name`` argument is used.
+
+ ``alt_for``
+ Designate a method as another view for the specified action if
+ the decorated method is not the desired action name instead of registering
+ the method with an action of the same name.
+
+ ``format``
+ Specify a format for the view that this decorator describes.
+ """
+ def __init__(self, **kw):
+ self.kw = kw
+
+ def __call__(self, wrapped):
+ if hasattr(wrapped, '__exposed__'):
+ wrapped.__exposed__.append(self.kw)
+ else:
+ wrapped.__exposed__ = [self.kw]
+ return wrapped
+
+# map.resource port
+def add_resource(self, handler, member_name, collection_name, **kwargs):
+ """ Add some RESTful routes for a resource handler.
+
+ This function should never be called directly; instead the
+ ``pyramid_routehelper.includeme`` function should be used to include this
+ function into an application; the function will thereafter be available
+ as a method of the resulting configurator.
+
+ The concept of a web resource maps somewhat directly to 'CRUD'
+ operations. The overlying things to keep in mind is that
+ adding a resource handler is about handling creating, viewing, and
+ editing that resource.
+
+ ``handler`` is a dotted name of (or direct reference to) a
+ Python handler class,
+ e.g. ``'my.package.handlers.MyHandler'``.
+
+ ``member_name`` should be the appropriate singular version of the resource
+ given your locale and used with members of the collection.
+
+ ``collection_name`` will be used to refer to the resource collection methods
+ and should be a plural version of the member_name argument.
+
+ All keyword arguments are optional.
+
+ ``collection``
+ Additional action mappings used to manipulate/view the
+ entire set of resources provided by the handler.
+
+ Example::
+
+ config.add_resource('myproject.handlers:MessageHandler', 'message', 'messages', collection={'rss':'GET'})
+ # GET /messages/rss (maps to the rss action)
+ # also adds named route "rss_message"
+
+ ``member``
+ Additional action mappings used to access an individual
+ 'member' of this handler's resources.
+
+ Example::
+
+ config.add_resource('myproject.handlers:MessageHandler', 'message', 'messages', member={'mark':'POST'})
+ # POST /messages/1/mark (maps to the mark action)
+ # also adds named route "mark_message"
+
+ ``new``
+ Action mappings that involve dealing with a new member in
+ the controller resources.
+
+ Example::
+
+ config.add_resource('myproject.handlers:MessageHandler', 'message', 'messages', new={'preview':'POST'})
+ # POST /messages/new/preview (maps to the preview action)
+ # also adds a url named "preview_new_message"
+
+ ``path_prefix``
+ Prepends the URL path for the Route with the path_prefix
+ given. This is most useful for cases where you want to mix
+ resources or relations between resources.
+
+ ``name_prefix``
+ Perpends the route names that are generated with the
+ name_prefix given. Combined with the path_prefix option,
+ it's easy to generate route names and paths that represent
+ resources that are in relations.
+
+ Example::
+
+ config.add_resource('myproject.handlers:CategoryHandler', 'message', 'messages',
+ path_prefix='/category/:category_id',
+ name_prefix="category_")
+ # GET /category/7/messages/1
+ # has named route "category_message"
+
+ ``parent_resource``
+ A ``dict`` containing information about the parent
+ resource, for creating a nested resource. It should contain
+ the ``member_name`` and ``collection_name`` of the parent
+ resource.
+
+ If ``parent_resource`` is supplied and ``path_prefix``
+ isn't, ``path_prefix`` will be generated from
+ ``parent_resource`` as
+ "<parent collection name>/:<parent member name>_id".
+
+ If ``parent_resource`` is supplied and ``name_prefix``
+ isn't, ``name_prefix`` will be generated from
+ ``parent_resource`` as "<parent member name>_".
+
+ Example::
+
+ >>> from pyramid.url import route_path
+ >>> config.add_resource('myproject.handlers:LocationHandler', 'location', 'locations',
+ ... parent_resource=dict(member_name='region',
+ ... collection_name='regions'))
+ >>> # path_prefix is "regions/:region_id"
+ >>> # name prefix is "region_"
+ >>> route_path('region_locations', region_id=13)
+ '/regions/13/locations'
+ >>> route_path('region_new_location', region_id=13)
+ '/regions/13/locations/new'
+ >>> route_path('region_location', region_id=13, id=60)
+ '/regions/13/locations/60'
+ >>> route_path('region_edit_location', region_id=13, id=60)
+ '/regions/13/locations/60/edit'
+
+ Overriding generated ``path_prefix``::
+
+ >>> config.add_resource('myproject.handlers:LocationHandler', 'location', 'locations',
+ ... parent_resource=dict(member_name='region',
+ ... collection_name='regions'),
+ ... path_prefix='areas/:area_id')
+ >>> # name prefix is "region_"
+ >>> route_path('region_locations', area_id=51)
+ '/areas/51/locations'
+
+ Overriding generated ``name_prefix``::
+
+ >>> config.add_resource('myproject.handlers:LocationHandler', 'location', 'locations',
+ ... parent_resource=dict(member_name='region',
+ ... collection_name='regions'),
+ ... name_prefix='')
+ >>> # path_prefix is "regions/:region_id"
+ >>> route_path('locations', region_id=51)
+ '/regions/51/locations'
+ """
+ handler = self.maybe_dotted(handler)
+
+ action_kwargs = {}
+ for name,meth in inspect.getmembers(handler, inspect.ismethod):
+ if hasattr(meth, '__exposed__'):
+ for settings in meth.__exposed__:
+ config_settings = settings.copy()
+ action_name = config_settings.pop('alt_for', name)
+
+ # If format is not set, use the route that doesn't specify a format
+ if 'format' not in config_settings:
+ if 'default' in action_kwargs.get(action_name,{}):
+ raise ConfigurationError("Two methods have been decorated without specifying a format.")
+ else:
+ action_kwargs.setdefault(action_name, {})['default'] = config_settings
+ # Otherwise, append to the list of view config settings for formatted views
+ else:
+ config_settings['attr'] = name
+ action_kwargs.setdefault(action_name, {}).setdefault('formatted',[]).append(config_settings)
+
+ collection = kwargs.pop('collection', {})
+ member = kwargs.pop('member', {})
+ new = kwargs.pop('new', {})
+ path_prefix = kwargs.pop('path_prefix', None)
+ name_prefix = kwargs.pop('name_prefix', None)
+ parent_resource = kwargs.pop('parent_resource', None)
+
+ if parent_resource is not None:
+ if path_prefix is None:
+ path_prefix = '%s/:%s_id' % (parent_resource['collection_name'], parent_resource['member_name'])
+ if name_prefix is None:
+ name_prefix = '%s_' % parent_resource['member_name']
+ else:
+ if path_prefix is None: path_prefix = ''
+ if name_prefix is None: name_prefix = ''
+
+ member['edit'] = 'GET'
+ new['new'] = 'GET'
+
+ def swap(dct, newdct):
+ map(lambda (key,value): newdct.setdefault(value.upper(), []).append(key), dct.items())
+ return newdct
+
+ collection_methods = swap(collection, {})
+ member_methods = swap(member, {})
+ new_methods = swap(new, {})
+
+ collection_methods.setdefault('POST', []).insert(0, 'create')
+ member_methods.setdefault('PUT', []).insert(0, 'update')
+ member_methods.setdefault('DELETE', []).insert(0, 'delete')
+
+ # Continue porting code
+ controller = strip_slashes(collection_name)
+ path_prefix = strip_slashes(path_prefix)
+ path_prefix = '/' + path_prefix
+ if path_prefix and path_prefix != '/':
+ path = path_prefix + '/' + controller
+ else:
+ path = '/' + controller
+ collection_path = path
+ new_path = path + '/new'
+ member_path = path + '/:id'
+
+ def add_route_and_view(self, action, route_name, path, request_method='any'):
+ if request_method != 'any':
+ request_method = request_method.upper()
+ else:
+ request_method = None
+
+ self.add_route(route_name, path, **kwargs)
+ self.add_view(view=handler, attr=action, route_name=route_name, request_method=request_method, **action_kwargs.get(action, {}).get('default', {}))
+
+ for format_kwargs in action_kwargs.get(action, {}).get('formatted', []):
+ format = format_kwargs.pop('format')
+ self.add_route("%s_formatted_%s" % (format, route_name),
+ "%s.%s" % (path, format), **kwargs)
+ self.add_view(view=handler, attr=format_kwargs.pop('attr'), request_method=request_method,
+ route_name = "%s_formatted_%s" % (format, route_name), **format_kwargs)
+
+ for method, lst in collection_methods.iteritems():
+ primary = (method != 'GET' and lst.pop(0)) or None
+ for action in lst:
+ add_route_and_view(self, action, "%s%s_%s" % (name_prefix, action, collection_name), "%s/%s" % (collection_path,action))
+
+ if primary:
+ add_route_and_view(self, primary, name_prefix + collection_name, collection_path, method)
+
+ # Add route and view for collection
+ add_route_and_view(self, 'index', name_prefix + collection_name, collection_path, 'GET')
+
+ for method, lst in new_methods.iteritems():
+ for action in lst:
+ path = (action == 'new' and new_path) or "%s/%s" % (new_path, action)
+ name = "new_" + member_name
+ if action != 'new':
+ name = action + "_" + name
+ formatted_path = (action == 'new' and new_path + '.:format') or "%s/%s.:format" % (new_path, action)
+ add_route_and_view(self, action, name_prefix + name, path, method)
+
+ for method, lst in member_methods.iteritems():
+ if method not in ['POST', 'GET', 'any']:
+ primary = lst.pop(0)
+ else:
+ primary = None
+ for action in lst:
+ add_route_and_view(self, action, '%s%s_%s' % (name_prefix, action, member_name), '%s/%s' % (member_path, action))
+
+ if primary:
+ add_route_and_view(self, primary, name_prefix + member_name, member_path, method)
+
+ add_route_and_view(self, 'show', name_prefix + member_name, member_path, method)
+
+# Submapper support
+
+
+# Sub_domain option
+
+
+# Converters??
View
241 pyramid_routehelper/tests.py
@@ -0,0 +1,241 @@
+import unittest
+from pyramid import testing
+from pyramid.config import Configurator
+from pyramid_routehelper import includeme, add_resource, action, ConfigurationError
+from pyramid.url import route_path
+
+
+class TestResourceGeneration_add_resource(unittest.TestCase):
+ def _create_config(self, autocommit=True):
+ config = Configurator(autocommit=autocommit)
+ includeme(config)
+ return config
+
+ def setUp(self):
+ self.config = self._create_config()
+ self.config.begin()
+
+ def tearDown(self):
+ self.config.end()
+ del self.config
+
+ def test_basic_resources(self):
+ self.config.add_resource('pyramid_routehelper.tests:DummyCrudHandler', 'message', 'messages')
+
+ assert route_path('messages', testing.DummyRequest()) == '/messages'
+ assert route_path('json_formatted_messages', testing.DummyRequest()) == '/messages.json'
+ assert route_path('new_message', testing.DummyRequest()) == '/messages/new'
+ assert route_path('json_formatted_new_message', testing.DummyRequest()) == '/messages/new.json'
+
+ assert route_path('json_formatted_message', testing.DummyRequest(), id=1) == '/messages/1.json'
+ assert route_path('message', testing.DummyRequest(), id=1) == '/messages/1'
+ assert route_path('json_formatted_edit_message', testing.DummyRequest(), id=1) == '/messages/1/edit.json'
+ assert route_path('edit_message', testing.DummyRequest(), id=1) == '/messages/1/edit'
+
+ def test_resources_with_path_prefix(self):
+ self.config.add_resource('pyramid_routehelper.tests:DummyCrudHandler', 'message', 'messages', path_prefix='/category/:category_id')
+
+ assert route_path('messages', testing.DummyRequest(), category_id=2) == '/category/2/messages'
+ assert route_path('json_formatted_messages', testing.DummyRequest(), category_id=2) == '/category/2/messages.json'
+ assert route_path('new_message', testing.DummyRequest(), category_id=2) == '/category/2/messages/new'
+ assert route_path('json_formatted_new_message', testing.DummyRequest(), category_id=2) == '/category/2/messages/new.json'
+
+ assert route_path('json_formatted_message', testing.DummyRequest(), id=1, category_id=2) == '/category/2/messages/1.json'
+ assert route_path('message', testing.DummyRequest(), id=1, category_id=2) == '/category/2/messages/1'
+ assert route_path('json_formatted_edit_message', testing.DummyRequest(), id=1, category_id=2) == '/category/2/messages/1/edit.json'
+ assert route_path('edit_message', testing.DummyRequest(), id=1, category_id=2) == '/category/2/messages/1/edit'
+
+ def test_resources_with_path_prefix_with_trailing_slash(self):
+ self.config.add_resource('pyramid_routehelper.tests:DummyCrudHandler', 'message', 'messages', path_prefix='/category/:category_id/')
+
+ assert route_path('messages', testing.DummyRequest(), category_id=2) == '/category/2/messages'
+ assert route_path('json_formatted_messages', testing.DummyRequest(), category_id=2) == '/category/2/messages.json'
+ assert route_path('new_message', testing.DummyRequest(), category_id=2) == '/category/2/messages/new'
+ assert route_path('json_formatted_new_message', testing.DummyRequest(), category_id=2) == '/category/2/messages/new.json'
+
+ assert route_path('json_formatted_message', testing.DummyRequest(), id=1, category_id=2) == '/category/2/messages/1.json'
+ assert route_path('message', testing.DummyRequest(), id=1, category_id=2) == '/category/2/messages/1'
+ assert route_path('json_formatted_edit_message', testing.DummyRequest(), id=1, category_id=2) == '/category/2/messages/1/edit.json'
+ assert route_path('edit_message', testing.DummyRequest(), id=1, category_id=2) == '/category/2/messages/1/edit'
+
+ def test_resources_with_collection_action(self):
+ self.config.add_resource('pyramid_routehelper.tests:DummyCrudHandler', 'message', 'messages', collection=dict(sorted='GET'))
+
+ assert route_path('sorted_messages', testing.DummyRequest()) == '/messages/sorted'
+
+ def test_resources_with_member_action(self):
+ self.config.add_resource('pyramid_routehelper.tests:DummyCrudHandler', 'message', 'messages', member=dict(comment='GET'))
+
+ assert route_path('comment_message', testing.DummyRequest(), id=1) == '/messages/1/comment'
+
+ def test_resources_with_new_action(self):
+ self.config.add_resource('pyramid_routehelper.tests:DummyCrudHandler', 'message', 'messages', new=dict(preview='GET'))
+
+ assert route_path('preview_new_message', testing.DummyRequest(), id=1) == '/messages/new/preview'
+
+ def test_resources_with_name_prefix(self):
+ self.config.add_resource('pyramid_routehelper.tests:DummyCrudHandler', 'message', 'messages', name_prefix="special_")
+
+ assert route_path('special_message', testing.DummyRequest(), id=1) == '/messages/1'
+
+ def test_resources_with_parent_resource(self):
+ self.config.add_resource('pyramid_routehelper.tests:DummyCrudHandler',
+ 'message', 'messages',
+ parent_resource = dict(member_name='category', collection_name='categories'))
+
+ assert route_path('category_messages', testing.DummyRequest(), category_id=2) == '/categories/2/messages'
+ assert route_path('category_message', testing.DummyRequest(), category_id=2, id=1) == '/categories/2/messages/1'
+
+ def test_resources_with_parent_resource_override_path_prefix(self):
+ self.config.add_resource('pyramid_routehelper.tests:DummyCrudHandler',
+ 'message', 'messages',
+ parent_resource = dict(member_name='category', collection_name='categories'),
+ path_prefix = 'folders/:folder_id')
+
+ assert route_path('category_messages', testing.DummyRequest(), folder_id=2) == '/folders/2/messages'
+ assert route_path('category_message', testing.DummyRequest(), folder_id=2, id=1) == '/folders/2/messages/1'
+
+ def test_resources_with_parent_resource_override_name_prefix(self):
+ self.config.add_resource('pyramid_routehelper.tests:DummyCrudHandler',
+ 'message', 'messages',
+ parent_resource = dict(member_name='category', collection_name='categories'),
+ name_prefix = '')
+
+ assert route_path('messages', testing.DummyRequest(), category_id=2) == '/categories/2/messages'
+ assert route_path('message', testing.DummyRequest(), category_id=2, id=1) == '/categories/2/messages/1'
+
+ def test_resources_with_double_default_views(self):
+ class MessedUpHandler(object):
+ @action(renderer='json')
+ @action(renderer='template.mak')
+ def index(self):
+ return {}
+
+ try:
+ self.config.add_resource(MessedUpHandler, 'message', 'messages')
+ except ConfigurationError, e:
+ assert str(e) == "Two methods have been decorated without specifying a format."
+
+class TestResourceRecognition(unittest.TestCase):
+ def _create_config(self, autocommit=True):
+ config = Configurator(autocommit=autocommit)
+ includeme(config)
+ return config
+
+ def setUp(self):
+ self.config = self._create_config()
+ self.config.add_resource('pyramid_routehelper.tests:DummyCrudHandler', 'message', 'messages')
+ self.config.begin()
+
+ self.wsgi_app = self.config.make_wsgi_app()
+
+ self.collection_path = '/messages'
+ self.collection_name = 'messages'
+ self.member_path = '/messages/:id'
+ self.member_name = 'message'
+
+ def tearDown(self):
+ self.config.end()
+ del self.config
+
+ def _get(self, path):
+ return self._makeRequest(path, 'GET')
+
+ def _post(self, path):
+ return self._makeRequest(path, 'POST')
+
+ def _put(self, path):
+ return self._makeRequest(path, 'PUT')
+
+ def _delete(self, path):
+ return self._makeRequest(path, 'DELETE')
+
+ def _makeRequest(self, path, request_method = 'GET'):
+ wsgi_environ = dict(
+ PATH_INFO = path,
+ REQUEST_METHOD = request_method
+ )
+ resp_body = self.wsgi_app(wsgi_environ, lambda status,headers: None)[0]
+ return resp_body
+
+ def test_get_collection(self):
+ result = self._get('/messages')
+ assert result == 'index'
+
+ def test_get_formatted_collection(self):
+ result = self._get('/messages.json')
+ assert result == '{"format": "json"}'
+
+ def test_post_collection(self):
+ result = self._post('/messages')
+ assert result == 'create'
+
+ def test_get_member(self):
+ result = self._get('/messages/1')
+ assert result == 'show'
+
+ def test_put_member(self):
+ result = self._put('/messages/1')
+ assert result == 'update'
+
+ def test_delete_member(self):
+ result = self._delete('/messages/1')
+ assert result == 'delete'
+
+ def test_new_member(self):
+ result = self._get('/messages/new')
+ assert result == 'new'
+
+ def test_edit_member(self):
+ result = self._get('/messages/1/edit')
+ assert result == 'edit'
+
+class Test_includeme(unittest.TestCase):
+ def test_includme(self):
+ config = Configurator(autocommit=True)
+ includeme(config)
+ assert config.add_resource.im_func.__docobj__ is add_resource
+
+class DummyCrudHandler(object):
+ def __init__(self, request):
+ self.request = request
+
+ @action(renderer='string')
+ def index(self):
+ return "index"
+
+ @action(alt_for='index', renderer='xml', xhr=True, format='xml')
+ @action(alt_for='index', renderer='json', format='json')
+ def api_index(self):
+ return {'format':'json'}
+
+ @action(renderer='string')
+ def create(self):
+ return "create"
+
+ @action(renderer='json', format='json')
+ @action(renderer='string')
+ def show(self):
+ return "show"
+
+ @action(renderer='string')
+ def update(self):
+ return "update"
+
+ @action(renderer='string')
+ def delete(self):
+ return "delete"
+
+ @action(renderer='json', format='json')
+ @action(renderer='string')
+ def new(self):
+ return "new"
+
+ @action(renderer='json', format='json')
+ @action(renderer='string')
+ def edit(self):
+ return "edit"
+
+ @action(renderer='string')
+ def sorted(self):
+ return "sorted"
Something went wrong with that request. Please try again.