summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@plope.com>2011-12-14 03:41:37 -0500
committerChris McDonough <chrism@plope.com>2011-12-14 03:41:37 -0500
commita1995a197b735b2d1bbf674a5a6a82c359b5131f (patch)
treee9b7f2f8ee0c892bbf6d35cd5d377a0193cd67ef
parentec507ade5541e1e3949fd6020062e78d113e0ed8 (diff)
parent4375cf2bad3535ce896e95fcf1e388e33f2e8ecf (diff)
downloadpyramid-a1995a197b735b2d1bbf674a5a6a82c359b5131f.tar.gz
pyramid-a1995a197b735b2d1bbf674a5a6a82c359b5131f.tar.bz2
pyramid-a1995a197b735b2d1bbf674a5a6a82c359b5131f.zip
Merge branch 'feature.viewdefaults'
-rw-r--r--CHANGES.txt20
-rw-r--r--docs/api/view.rst3
-rw-r--r--docs/narr/viewconfig.rst181
-rw-r--r--docs/whatsnew-1.3.rst64
-rw-r--r--pyramid/config/util.py4
-rw-r--r--pyramid/config/views.py16
-rw-r--r--pyramid/tests/test_config/test_views.py27
-rw-r--r--pyramid/tests/test_view.py34
-rw-r--r--pyramid/view.py13
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