diff options
| author | Chris McDonough <chrism@plope.com> | 2011-12-14 03:41:37 -0500 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2011-12-14 03:41:37 -0500 |
| commit | a1995a197b735b2d1bbf674a5a6a82c359b5131f (patch) | |
| tree | e9b7f2f8ee0c892bbf6d35cd5d377a0193cd67ef | |
| parent | ec507ade5541e1e3949fd6020062e78d113e0ed8 (diff) | |
| parent | 4375cf2bad3535ce896e95fcf1e388e33f2e8ecf (diff) | |
| download | pyramid-a1995a197b735b2d1bbf674a5a6a82c359b5131f.tar.gz pyramid-a1995a197b735b2d1bbf674a5a6a82c359b5131f.tar.bz2 pyramid-a1995a197b735b2d1bbf674a5a6a82c359b5131f.zip | |
Merge branch 'feature.viewdefaults'
| -rw-r--r-- | CHANGES.txt | 20 | ||||
| -rw-r--r-- | docs/api/view.rst | 3 | ||||
| -rw-r--r-- | docs/narr/viewconfig.rst | 181 | ||||
| -rw-r--r-- | docs/whatsnew-1.3.rst | 64 | ||||
| -rw-r--r-- | pyramid/config/util.py | 4 | ||||
| -rw-r--r-- | pyramid/config/views.py | 16 | ||||
| -rw-r--r-- | pyramid/tests/test_config/test_views.py | 27 | ||||
| -rw-r--r-- | pyramid/tests/test_view.py | 34 | ||||
| -rw-r--r-- | pyramid/view.py | 13 |
9 files changed, 361 insertions, 1 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 9cdaac5be..c979c4dc1 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,23 @@ +Next release +============ + +Features +-------- + +- New API: ``pyramid.view.view_defaults``. If you use a class as a view, you + can use the new ``view_defaults`` class decorator on the class to provide + defaults to the view configuration information used by every + ``@view_config`` decorator that decorates a method of that class. It also + works against view configurations involving a class made imperatively. + +Documentation +------------- + +- Added documentation to "View Configuration" narrative documentation chapter + about ``view_defaults`` class decorator. + +- Added API docs for ``view_defaults`` class decorator. + 1.3a1 (2011-12-09) ================== diff --git a/docs/api/view.rst b/docs/api/view.rst index 4dddea25f..9f59ddae7 100644 --- a/docs/api/view.rst +++ b/docs/api/view.rst @@ -16,6 +16,9 @@ .. autoclass:: view_config :members: + .. autoclass:: view_defaults + :members: + .. autoclass:: static :members: :inherited-members: diff --git a/docs/narr/viewconfig.rst b/docs/narr/viewconfig.rst index af5d7f242..03000629c 100644 --- a/docs/narr/viewconfig.rst +++ b/docs/narr/viewconfig.rst @@ -621,6 +621,7 @@ against the ``amethod`` method could be spelled equivalently as the below: def amethod(self): return Response('hello') + .. index:: single: add_view @@ -658,6 +659,186 @@ configurations, you don't need to issue a :term:`scan` in order for the view configuration to take effect. .. index:: + single: view_defaults class decorator + +.. _view_defaults: + +``@view_defaults`` Class Decorator +---------------------------------- + +.. note:: + + This feature is new in Pyramid 1.3. + +If you use a class as a view, you can use the +:class:`pyramid.view.view_defaults` class decorator on the class to provide +defaults to the view configuration information used by every ``@view_config`` +decorator that decorates a method of that class. + +For instance, if you've got a class that has methods that represent "REST +actions", all which are mapped to the same route, but different request +methods, instead of this: + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + from pyramid.response import Response + + class RESTView(object): + def __init__(self, request): + self.request = request + + @view_config(route_name='rest', request_method='GET') + def get(self): + return Response('get') + + @view_config(route_name='rest', request_method='POST') + def post(self): + return Response('post') + + @view_config(route_name='rest', request_method='DELETE') + def delete(self): + return Response('delete') + +You can do this: + +.. code-block:: python + :linenos: + + from pyramid.view import view_defaults + from pyramid.view import view_config + from pyramid.response import Response + + @view_defaults(route_name='rest') + class RESTView(object): + def __init__(self, request): + self.request = request + + @view_config(request_method='GET') + def get(self): + return Response('get') + + @view_config(request_method='POST') + def post(self): + return Response('post') + + @view_config(request_method='DELETE') + def delete(self): + return Response('delete') + +In the above example, we were able to take the ``route_name='rest'`` argument +out of the call to each individual ``@view_config`` statement, because we +used a ``@view_defaults`` class decorator to provide the argument as a +default to each view method it possessed. + +Arguments passed to ``@view_config`` will override any default passed to +``@view_defaults``. + +The ``view_defaults`` class decorator can also provide defaults to the +:meth:`pyramid.config.Configurator.add_view` directive when a decorated class +is passed to that directive as its ``view`` argument. For example, instead +of this: + +.. code-block:: python + :linenos: + + from pyramid.response import Response + from pyramid.config import Configurator + + class RESTView(object): + def __init__(self, request): + self.request = request + + def get(self): + return Response('get') + + def post(self): + return Response('post') + + def delete(self): + return Response('delete') + + if __name__ == '__main__': + config = Configurator() + config.add_route('rest', '/rest') + config.add_view( + RESTView, route_name='rest', attr='get', request_method='GET') + config.add_view( + RESTView, route_name='rest', attr='post', request_method='POST') + config.add_view( + RESTView, route_name='rest', attr='delete', request_method='DELETE') + +To reduce the amount of repetion in the ``config.add_view`` statements, we +can move the ``route_name='rest'`` argument to a ``@view_default`` class +decorator on the RESTView class: + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + from pyramid.response import Response + from pyramid.config import Configurator + + @view_defaults(route_name='rest') + class RESTView(object): + def __init__(self, request): + self.request = request + + def get(self): + return Response('get') + + def post(self): + return Response('post') + + def delete(self): + return Response('delete') + + if __name__ == '__main__': + config = Configurator() + config.add_route('rest', '/rest') + config.add_view(RESTView, attr='get', request_method='GET') + config.add_view(RESTView, attr='post', request_method='POST') + config.add_view(RESTView, attr='delete', request_method='DELETE') + +:class:`pyramid.view.view_defaults` accepts the same set of arguments that +:class:`pyramid.view.view_config` does, and they have the same meaning. Each +argument passed to ``view_defaults`` provides a default for the view +configurations of methods of the class it's decorating. + +Normal Python inheritance rules apply to defaults added via +``view_defaults``. For example: + +.. code-block:: python + :linenos: + + @view_defaults(route_name='rest') + class Foo(object): + pass + + class Bar(Foo): + pass + +The ``Bar`` class above will inherit its view defaults from the arguments +passed to the ``view_defaults`` decorator of the ``Foo`` class. To prevent +this from happening, use a ``view_defaults`` decorator without any arguments +on the subclass: + +.. code-block:: python + :linenos: + + @view_defaults(route_name='rest') + class Foo(object): + pass + + @view_defaults() + class Bar(Foo): + pass + +The ``view_defaults`` decorator only works as a class decorator; using it +against a function or a method will produce nonsensical results. + +.. index:: single: view security pair: security; view diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst index f51c7977a..608db74cd 100644 --- a/docs/whatsnew-1.3.rst +++ b/docs/whatsnew-1.3.rst @@ -126,6 +126,70 @@ New APIs were added to support introspection :attr:`pyramid.config.Configurator.introspectable`, :attr:`pyramid.registry.Registry.introspector`. +``@view_defaults`` Decorator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you use a class as a view, you can use the new +:class:`pyramid.view.view_defaults` class decorator on the class to provide +defaults to the view configuration information used by every ``@view_config`` +decorator that decorates a method of that class. + +For instance, if you've got a class that has methods that represent "REST +actions", all which are mapped to the same route, but different request +methods, instead of this: + +.. code-block:: python + :linenos: + + from pyramid.view import view_config + from pyramid.response import Response + + class RESTView(object): + def __init__(self, request): + self.request = request + + @view_config(route_name='rest', request_method='GET') + def get(self): + return Response('get') + + @view_config(route_name='rest', request_method='POST') + def post(self): + return Response('post') + + @view_config(route_name='rest', request_method='DELETE') + def delete(self): + return Response('delete') + +You can do this: + +.. code-block:: python + :linenos: + + from pyramid.view import view_defaults + from pyramid.view import view_config + from pyramid.response import Response + + @view_defaults(route_name='rest') + class RESTView(object): + def __init__(self, request): + self.request = request + + @view_config(request_method='GET') + def get(self): + return Response('get') + + @view_config(request_method='POST') + def post(self): + return Response('post') + + @view_config(request_method='DELETE') + def delete(self): + return Response('delete') + +This also works for imperative view configurations that involve a class. + +See :ref:`view_defaults` for more information. + Minor Feature Additions ----------------------- diff --git a/pyramid/config/util.py b/pyramid/config/util.py index 3a2f911dc..b0e873de3 100644 --- a/pyramid/config/util.py +++ b/pyramid/config/util.py @@ -43,13 +43,15 @@ def action_method(wrapped): if self._ainfo is None: self._ainfo = [] info = kw.pop('_info', None) + # backframes for outer decorators to actionmethods + backframes = kw.pop('_backframes', 2) if is_nonstr_iter(info) and len(info) == 4: # _info permitted as extract_stack tuple info = ActionInfo(*info) if info is None: try: f = traceback.extract_stack(limit=3) - info = ActionInfo(*f[-2]) + info = ActionInfo(*f[-backframes]) except: # pragma: no cover info = ActionInfo(None, 0, '', '') self._ainfo.append(info) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index a4333b3d6..5efe1f2bb 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1,5 +1,6 @@ import inspect import operator +from functools import wraps from zope.interface import ( Interface, @@ -551,7 +552,22 @@ class MultiView(object): continue raise PredicateMismatch(self.name) +def viewdefaults(wrapped): + def wrapper(*arg, **kw): + defaults = {} + if len(arg) > 1: + view = arg[1] + else: + view = kw.get('view') + if inspect.isclass(view): + defaults = getattr(view, '__view_defaults__', {}).copy() + defaults.update(kw) + defaults['_backframes'] = 3 # for action_method + return wrapped(*arg, **defaults) + return wraps(wrapped)(wrapper) + class ViewsConfiguratorMixin(object): + @viewdefaults @action_method def add_view(self, view=None, name="", for_=None, permission=None, request_type=None, route_name=None, request_method=None, diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index a7b0dd574..30a30a1e8 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -1376,6 +1376,33 @@ class TestViewsConfigurationMixin(unittest.TestCase): self.assertEqual(view(None, None), 'OK') self.assertEqual(Mapper.kw['mapper'], Mapper) + def test_add_view_with_view_defaults(self): + from pyramid.renderers import null_renderer + from pyramid.exceptions import PredicateMismatch + from zope.interface import directlyProvides + class view(object): + __view_defaults__ = { + 'containment':'pyramid.tests.test_config.IDummy' + } + def __init__(self, request): + pass + def __call__(self): + return 'OK' + config = self._makeOne(autocommit=True) + config.add_view( + view=view, + renderer=null_renderer) + wrapper = self._getViewCallable(config) + context = DummyContext() + directlyProvides(context, IDummy) + request = self._makeRequest(config) + self.assertEqual(wrapper(context, request), 'OK') + + context = DummyContext() + request = self._makeRequest(config) + self.assertRaises(PredicateMismatch, wrapper, context, request) + + def test_derive_view_function(self): from pyramid.renderers import null_renderer def view(request): diff --git a/pyramid/tests/test_view.py b/pyramid/tests/test_view.py index 1bdb066c0..0d00e65c6 100644 --- a/pyramid/tests/test_view.py +++ b/pyramid/tests/test_view.py @@ -570,6 +570,40 @@ class Test_static(unittest.TestCase): view = self._makeOne(path, None) self.assertEqual(view.docroot, 'fixtures') +class Test_view_defaults(unittest.TestCase): + def test_it(self): + from pyramid.view import view_defaults + @view_defaults(route_name='abc', renderer='def') + class Foo(object): pass + self.assertEqual(Foo.__view_defaults__['route_name'],'abc') + self.assertEqual(Foo.__view_defaults__['renderer'],'def') + + def test_it_inheritance_not_overridden(self): + from pyramid.view import view_defaults + @view_defaults(route_name='abc', renderer='def') + class Foo(object): pass + class Bar(Foo): pass + self.assertEqual(Bar.__view_defaults__['route_name'],'abc') + self.assertEqual(Bar.__view_defaults__['renderer'],'def') + + def test_it_inheritance_overriden(self): + from pyramid.view import view_defaults + @view_defaults(route_name='abc', renderer='def') + class Foo(object): pass + @view_defaults(route_name='ghi') + class Bar(Foo): pass + self.assertEqual(Bar.__view_defaults__['route_name'],'ghi') + self.assertEqual(Bar.__view_defaults__['renderer'], None) + + def test_it_inheritance_overriden_empty(self): + from pyramid.view import view_defaults + @view_defaults(route_name='abc', renderer='def') + class Foo(object): pass + @view_defaults() + class Bar(Foo): pass + self.assertEqual(Bar.__view_defaults__['route_name'], None) + self.assertEqual(Bar.__view_defaults__['renderer'], None) + class ExceptionResponse(Exception): status = '404 Not Found' app_iter = ['Not Found'] diff --git a/pyramid/view.py b/pyramid/view.py index da5a71c4c..eae56a661 100644 --- a/pyramid/view.py +++ b/pyramid/view.py @@ -223,6 +223,19 @@ class view_config(object): bfg_view = view_config # bw compat (forever) +class view_defaults(view_config): + """ A class :term:`decorator` which, when applied to a class, will + provide defaults for all view configurations that use the class. This + decorator accepts all the arguments accepted by + :class:`pyramid.config.view_config`, and each has the same meaning. + + See :ref:`view_defaults` for more information. + """ + + def __call__(self, wrapped): + wrapped.__view_defaults__ = self.__dict__.copy() + return wrapped + class AppendSlashNotFoundViewFactory(object): """ There can only be one :term:`Not Found view` in any :app:`Pyramid` application. Even if you use |
