Chris McDonough
2011-12-14 4375cf2bad3535ce896e95fcf1e388e33f2e8ecf
Flesh out new view_defaults feature and add docs, change notes, and add to whatsnew.
7 files modified
298 ■■■■■ changed files
CHANGES.txt 20 ●●●●● patch | view | raw | blame | history
docs/api/view.rst 3 ●●●●● patch | view | raw | blame | history
docs/narr/viewconfig.rst 181 ●●●●● patch | view | raw | blame | history
docs/whatsnew-1.3.rst 64 ●●●●● patch | view | raw | blame | history
pyramid/config/views.py 2 ●●● patch | view | raw | blame | history
pyramid/tests/test_view.py 20 ●●●●● patch | view | raw | blame | history
pyramid/view.py 8 ●●●●● patch | view | raw | blame | history
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)
==================
docs/api/view.rst
@@ -16,6 +16,9 @@
  .. autoclass:: view_config
     :members:
  .. autoclass:: view_defaults
     :members:
  .. autoclass:: static
     :members:
     :inherited-members:
docs/narr/viewconfig.rst
@@ -621,6 +621,7 @@
       def amethod(self):
           return Response('hello')
.. index::
   single: add_view
@@ -658,6 +659,186 @@
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
docs/whatsnew-1.3.rst
@@ -126,6 +126,70 @@
: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
-----------------------
pyramid/config/views.py
@@ -562,7 +562,7 @@
        if inspect.isclass(view):
            defaults = getattr(view, '__view_defaults__', {}).copy()
        defaults.update(kw)
        defaults['_backframes'] = 3
        defaults['_backframes'] = 3 # for action_method
        return wrapped(*arg, **defaults)
    return wraps(wrapped)(wrapper)
pyramid/tests/test_view.py
@@ -578,7 +578,7 @@
        self.assertEqual(Foo.__view_defaults__['route_name'],'abc')
        self.assertEqual(Foo.__view_defaults__['renderer'],'def')
    def test_it_single_inheritance_non_overridden(self):
    def test_it_inheritance_not_overridden(self):
        from pyramid.view import view_defaults
        @view_defaults(route_name='abc', renderer='def')
        class Foo(object): pass
@@ -586,6 +586,24 @@
        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']
pyramid/view.py
@@ -224,6 +224,14 @@
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