summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@agendaless.com>2009-05-21 16:01:58 +0000
committerChris McDonough <chrism@agendaless.com>2009-05-21 16:01:58 +0000
commit5a11e2ad0828b7c763d0c81211f686a85bc0324c (patch)
tree750deaa5086279a1cd0baa28c0d5bdaa17414463
parent385084582eeff5f2f1a93f3b90c091dc1a4ad50e (diff)
downloadpyramid-5a11e2ad0828b7c763d0c81211f686a85bc0324c.tar.gz
pyramid-5a11e2ad0828b7c763d0c81211f686a85bc0324c.tar.bz2
pyramid-5a11e2ad0828b7c763d0c81211f686a85bc0324c.zip
- Class objects may now be used as view callables (both via ZCML and
via use of the ``bfg_view`` decorator in Python 2.6 as a class decorator). The calling semantics when using a class as a view callable is similar to that of using a class as a Zope "browser view": the class' ``__init__`` must accept two positional parameters (conventionally named ``context``, and ``request``). The resulting instance must be callable (it must have a ``__call__`` method). When called, the instance should return a response. For example:: from webob import Response class MyView(object): def __init__(self, context, request): self.context = context self.request = request def __call__(self): return Response('hello from %s!' % self.context) See the "Views" chapter in the documentation and the ``repoze.bfg.view`` API documentation for more information.
-rw-r--r--CHANGES.txt22
-rw-r--r--docs/narr/views.rst107
-rw-r--r--repoze/bfg/tests/grokkedapp/__init__.py15
-rw-r--r--repoze/bfg/tests/test_integration.py12
-rw-r--r--repoze/bfg/tests/test_view.py54
-rw-r--r--repoze/bfg/tests/test_zcml.py53
-rw-r--r--repoze/bfg/view.py59
-rw-r--r--repoze/bfg/zcml.py19
8 files changed, 317 insertions, 24 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 817b51aff..3233922bd 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -4,6 +4,28 @@
Features
--------
+- Class objects may now be used as view callables (both via ZCML and
+ via use of the ``bfg_view`` decorator in Python 2.6 as a class
+ decorator). The calling semantics when using a class as a view
+ callable is similar to that of using a class as a Zope "browser
+ view": the class' ``__init__`` must accept two positional parameters
+ (conventionally named ``context``, and ``request``). The resulting
+ instance must be callable (it must have a ``__call__`` method).
+ When called, the instance should return a response. For example::
+
+ from webob import Response
+
+ class MyView(object):
+ def __init__(self, context, request):
+ self.context = context
+ self.request = request
+
+ def __call__(self):
+ return Response('hello from %s!' % self.context)
+
+ See the "Views" chapter in the documentation and the
+ ``repoze.bfg.view`` API documentation for more information.
+
- Removed the pickling of ZCML actions (the code that wrote
``configure.zcml.cache`` next to ``configure.zcml`` files in
projects). The code which managed writing and reading of the cache
diff --git a/docs/narr/views.rst b/docs/narr/views.rst
index ecaa9784b..1bc6b4419 100644
--- a/docs/narr/views.rst
+++ b/docs/narr/views.rst
@@ -8,6 +8,8 @@ your application. :mod:`repoze.bfg's` primary job is to find and call
a view when a :term:`request` reaches it. The view's return value
must implement the :term:`WebOb` ``Response`` object interface.
+.. _function_as_view:
+
Defining a View as a Function
-----------------------------
@@ -18,12 +20,13 @@ this is a hello world view implemented as a function:
.. code-block:: python
:linenos:
+ from webob import Response
+
def hello_world(context, request):
- from webob import Response
return Response('Hello world!')
-The :term:`context` and :term:`request` arguments can be defined as
-follows:
+The :term:`context` and :term:`request` arguments passed to a view
+function can be defined as follows:
context
@@ -34,10 +37,55 @@ request
A WebOb request object representing the current WSGI request.
-A view must return an object that implements the :term:`WebOb`
-``Response`` interface. The easiest way to return something that
-implements this interface is to return a ``webob.Response`` object.
-But any object that has the following attributes will work:
+.. _class_as_view:
+
+Defining a View as a Class
+--------------------------
+
+.. note:: This feature is new as of :mod:`repoze.bfg` 0.8.1.
+
+When a view callable is a class, the calling semantics are slightly
+different than when it is a function or another non-class callable.
+When a view is a class, the class' ``__init__`` is called with the
+context and the request parameters. As a result, an instance of the
+class is created. Subsequently, that instance's ``__call__`` method
+is invoked with no parameters. The class' ``__call__`` method must
+return a response. This provides behavior similar to a Zope 'browser
+view' (Zope 'browser views' are typically classes instead of simple
+callables). So the simplest class that can be a view must have:
+
+- an ``__init__`` method that accepts a ``context`` and a ``request``
+ as positional arguments.
+
+- a ``__call__`` method that accepts no parameters and returns a
+ response.
+
+For example:
+
+.. code-block:: python
+ :linenos:
+
+ from webob import Response
+
+ class MyView(object):
+ def __init__(self, context, request):
+ self.context = context
+ self.request = request
+
+ def __call__(self):
+ return Response('hello from %r!' % self.context)
+
+The context and request objects passed to ``__init__`` are the same
+types of objects as described in :ref:`function_as_view`.
+
+The Response
+------------
+
+A view callable must return an object that implements the
+:term:`WebOb` ``Response`` interface. The easiest way to return
+something that implements this interface is to return a
+``webob.Response`` object. But any object that has the following
+attributes will work:
status
@@ -127,6 +175,10 @@ This indicates that when :mod:`repoze.bfg` identifies that the *view
name* is ``hello.html`` against *any* :term:`context`, this view will
be called.
+A ZCML ``view`` declaration's ``view`` attribute can also name a
+class. In this case, the rules described in :ref:`class_as_view`
+apply for the class which is named.
+
The ``view`` ZCML Directive
~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -260,6 +312,47 @@ name will be ``my_view``, registered for models with the
no permission, registered against requests which implement the default
``IRequest`` interface.
+If your view callable is a class, the ``bfg_view`` decorator can also
+be used as a class decorator in Python 2.6 and better (Python 2.5 and
+below do not support class decorators). All the arguments to the
+decorator are the same when applied against a class as when they are
+applied against a function. For example:
+
+.. code-block:: python
+ :linenos:
+
+ from webob import Response
+ from repoze.bfg.view import bfg_view
+
+ @bfg_view()
+ class MyView(object):
+ def __init__(self, context, request):
+ self.context = context
+ self.request = request
+
+ def __call__(self):
+ return Response('hello from %s!' % self.context)
+
+You can use the ``bfg_view`` decorator as a simple callable to
+manually decorate classes in Python 2.5 and below (without the
+decorator syntactic sugar), if you wish:
+
+.. code-block:: python
+ :linenos:
+
+ from webob import Response
+ from repoze.bfg.view import bfg_view
+
+ class MyView(object):
+ def __init__(self, context, request):
+ self.context = context
+ self.request = request
+
+ def __call__(self):
+ return Response('hello from %s!' % self.context)
+
+ my_view = bfg_view()(MyView)
+
.. _using_model_interfaces:
Using Model Interfaces
diff --git a/repoze/bfg/tests/grokkedapp/__init__.py b/repoze/bfg/tests/grokkedapp/__init__.py
index 9d91eb80f..aeedd4dba 100644
--- a/repoze/bfg/tests/grokkedapp/__init__.py
+++ b/repoze/bfg/tests/grokkedapp/__init__.py
@@ -8,4 +8,19 @@ class INothing(Interface):
def grokked(context, request):
""" """
+class grokked_klass(object):
+ """ """
+ def __init__(self, context, request):
+ self.context = context
+ self.request = request
+ def __call__(self):
+ """ """
+
+# in 2.6+ the below can be spelled as a class decorator:
+#
+# @bfg_view(for_=INothing, name='grokked_klass')
+# class grokked_class(object):
+# ....
+#
+grokked_klass = bfg_view(for_=INothing, name='grokked_klass')(grokked_klass)
diff --git a/repoze/bfg/tests/test_integration.py b/repoze/bfg/tests/test_integration.py
index 8c088fce0..fdb85b87e 100644
--- a/repoze/bfg/tests/test_integration.py
+++ b/repoze/bfg/tests/test_integration.py
@@ -1,4 +1,5 @@
import os
+import sys
import unittest
from repoze.bfg.push import pushpage
@@ -142,8 +143,8 @@ class TestGrokkedApp(unittest.TestCase):
cleanUp()
def test_it(self):
+ import inspect
import repoze.bfg.tests.grokkedapp as package
-
from zope.configuration import config
from zope.configuration import xmlconfig
context = config.ConfigurationMachine()
@@ -151,7 +152,14 @@ class TestGrokkedApp(unittest.TestCase):
context.package = package
xmlconfig.include(context, 'configure.zcml', package)
actions = context.actions
- self.failUnless(actions)
+ klassview = actions[-1]
+ self.assertEqual(klassview[0][2], 'grokked_klass')
+ self.assertEqual(klassview[2][1], package.grokked_klass)
+ self.failUnless(inspect.isfunction(package.grokked_klass))
+ self.assertEqual(package.grokked_klass(None, None), None)
+ funcview = actions[-2]
+ self.assertEqual(funcview[0][2], '')
+ self.assertEqual(funcview[2][1], package.grokked)
class DummyContext:
pass
diff --git a/repoze/bfg/tests/test_view.py b/repoze/bfg/tests/test_view.py
index a96051226..08beffeaa 100644
--- a/repoze/bfg/tests/test_view.py
+++ b/repoze/bfg/tests/test_view.py
@@ -446,13 +446,60 @@ class TestBFGViewDecorator(unittest.TestCase):
self.assertEqual(decorator.for_, None)
self.assertEqual(decorator.permission, 'foo')
- def test_call(self):
+ def test_call_function(self):
+ from repoze.bfg.interfaces import IRequest
+ from zope.interface import Interface
+ decorator = self._makeOne()
+ def foo():
+ """ docstring """
+ wrapped = decorator(foo)
+ self.failUnless(wrapped is foo)
+ self.assertEqual(wrapped.__is_bfg_view__, True)
+ self.assertEqual(wrapped.__permission__, None)
+ self.assertEqual(wrapped.__for__, Interface)
+ self.assertEqual(wrapped.__request_type__, IRequest)
+
+ def test_call_oldstyle_class(self):
+ import inspect
from repoze.bfg.interfaces import IRequest
from zope.interface import Interface
decorator = self._makeOne()
class foo:
""" docstring """
+ def __init__(self, context, request):
+ self.context = context
+ self.request = request
+ def __call__(self):
+ return self
+ wrapped = decorator(foo)
+ self.failIf(wrapped is foo)
+ self.failUnless(inspect.isfunction(wrapped))
+ self.assertEqual(wrapped.__is_bfg_view__, True)
+ self.assertEqual(wrapped.__permission__, None)
+ self.assertEqual(wrapped.__for__, Interface)
+ self.assertEqual(wrapped.__request_type__, IRequest)
+ self.assertEqual(wrapped.__module__, foo.__module__)
+ self.assertEqual(wrapped.__name__, foo.__name__)
+ self.assertEqual(wrapped.__doc__, foo.__doc__)
+ result = wrapped(None, None)
+ self.assertEqual(result.context, None)
+ self.assertEqual(result.request, None)
+
+ def test_call_newstyle_class(self):
+ import inspect
+ from repoze.bfg.interfaces import IRequest
+ from zope.interface import Interface
+ decorator = self._makeOne()
+ class foo(object):
+ """ docstring """
+ def __init__(self, context, request):
+ self.context = context
+ self.request = request
+ def __call__(self):
+ return self
wrapped = decorator(foo)
+ self.failIf(wrapped is foo)
+ self.failUnless(inspect.isfunction(wrapped))
self.assertEqual(wrapped.__is_bfg_view__, True)
self.assertEqual(wrapped.__permission__, None)
self.assertEqual(wrapped.__for__, Interface)
@@ -460,8 +507,9 @@ class TestBFGViewDecorator(unittest.TestCase):
self.assertEqual(wrapped.__module__, foo.__module__)
self.assertEqual(wrapped.__name__, foo.__name__)
self.assertEqual(wrapped.__doc__, foo.__doc__)
- for k, v in foo.__dict__.items():
- self.assertEqual(v, wrapped.__dict__[k])
+ result = wrapped(None, None)
+ self.assertEqual(result.context, None)
+ self.assertEqual(result.request, None)
class DummyContext:
pass
diff --git a/repoze/bfg/tests/test_zcml.py b/repoze/bfg/tests/test_zcml.py
index c2703f7c1..e44fe1307 100644
--- a/repoze/bfg/tests/test_zcml.py
+++ b/repoze/bfg/tests/test_zcml.py
@@ -19,7 +19,7 @@ class TestViewDirective(unittest.TestCase):
self.assertRaises(ConfigurationError, self._callFUT, context,
'repoze.view', None)
- def test_only_view(self):
+ def test_view_as_function(self):
context = DummyContext()
class IFoo:
pass
@@ -58,6 +58,57 @@ class TestViewDirective(unittest.TestCase):
self.assertEqual(regadapt['args'][4], '')
self.assertEqual(regadapt['args'][5], None)
+ def test_view_as_oldstyle_class(self):
+ context = DummyContext()
+ class IFoo:
+ pass
+ class view:
+ def __init__(self, context, request):
+ self.context = context
+ self.request = request
+
+ def __call__(self):
+ return self
+ self._callFUT(context, 'repoze.view', IFoo, view=view)
+ actions = context.actions
+ from repoze.bfg.interfaces import IRequest
+ from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IViewPermission
+ from repoze.bfg.security import ViewPermissionFactory
+ from repoze.bfg.zcml import handler
+
+ self.assertEqual(len(actions), 2)
+
+ permission = actions[0]
+ permission_discriminator = ('permission', IFoo, '', IRequest,
+ IViewPermission)
+ self.assertEqual(permission['discriminator'], permission_discriminator)
+ self.assertEqual(permission['callable'], handler)
+ self.assertEqual(permission['args'][0], 'registerAdapter')
+ self.failUnless(isinstance(permission['args'][1],ViewPermissionFactory))
+ self.assertEqual(permission['args'][1].permission_name, 'repoze.view')
+ self.assertEqual(permission['args'][2], (IFoo, IRequest))
+ self.assertEqual(permission['args'][3], IViewPermission)
+ self.assertEqual(permission['args'][4], '')
+ self.assertEqual(permission['args'][5], None)
+
+ regadapt = actions[1]
+ regadapt_discriminator = ('view', IFoo, '', IRequest, IView)
+ self.assertEqual(regadapt['discriminator'], regadapt_discriminator)
+ self.assertEqual(regadapt['callable'], handler)
+ self.assertEqual(regadapt['args'][0], 'registerAdapter')
+ wrapper = regadapt['args'][1]
+ self.assertEqual(wrapper.__module__, view.__module__)
+ self.assertEqual(wrapper.__name__, view.__name__)
+ self.assertEqual(wrapper.__doc__, view.__doc__)
+ result = wrapper(None, None)
+ self.assertEqual(result.context, None)
+ self.assertEqual(result.request, None)
+ self.assertEqual(regadapt['args'][2], (IFoo, IRequest))
+ self.assertEqual(regadapt['args'][3], IView)
+ self.assertEqual(regadapt['args'][4], '')
+ self.assertEqual(regadapt['args'][5], None)
+
def test_request_type(self):
context = DummyContext()
class IFoo:
diff --git a/repoze/bfg/view.py b/repoze/bfg/view.py
index a9a7cb973..a867987a5 100644
--- a/repoze/bfg/view.py
+++ b/repoze/bfg/view.py
@@ -1,3 +1,5 @@
+import inspect
+
from paste.urlparser import StaticURLParser
from zope.component import queryMultiAdapter
from zope.component import queryUtility
@@ -175,8 +177,8 @@ class static(object):
return request_copy.get_response(self.app)
class bfg_view(object):
- """ Decorator which allows Python code to make view registrations
- instead of using ZCML for the same purpose.
+ """ Function or class decorator which allows Python code to make
+ view registrations instead of using ZCML for the same purpose.
E.g. in the module ``views.py``::
@@ -222,8 +224,36 @@ class bfg_view(object):
registered against requests which implement the default IRequest
interface.
- To make use of bfg_view declarations, insert the following
- boilerplate into your application registry's ZCML::
+ The ``bfg_view`` decorator can also be used as a class decorator
+ in Python 2.6 and better (Python 2.5 and below do not support
+ class decorators)::
+
+ from webob import Response
+ from repoze.bfg.view import bfg_view
+
+ @bfg_view()
+ class MyView(object):
+ def __init__(self, context, request):
+ self.context = context
+ self.request = request
+ def __call__(self):
+ return Response('hello from %s!' % self.context)
+
+ .. warning:: This feature is new in 0.8.1.
+
+ .. note:: When a view is a class, the calling semantics are
+ different than when it is a function or another
+ non-class callable. When a view is a class, the class'
+ ``__init__`` is called with the context and the request
+ parameters, creating an instance. Subsequently that
+ instance's ``__call__`` method is invoked with no
+ parameters. The class' ``__call__`` method must return a
+ response. This provides behavior similar to a Zope
+ 'browser view' (Zope 'browser views' are typically classes
+ instead of simple callables).
+
+ To make use of any bfg_view declaration, you *must* insert the
+ following boilerplate into your application registry's ZCML::
<scan package="."/>
"""
@@ -235,16 +265,25 @@ class bfg_view(object):
self.permission = permission
def __call__(self, wrapped):
- def _bfg_view(context, request):
- return wrapped(context, request)
+ _bfg_view = wrapped
+ if inspect.isclass(_bfg_view):
+ # If the object we're decorating is a class, turn it into
+ # a function that operates like a Zope view (when it's
+ # invoked, construct an instance using 'context' and
+ # 'request' as position arguments, then immediately invoke
+ # the __call__ method of the instance with no arguments;
+ # __call__ should return an IResponse).
+ def _bfg_class_view(context, request):
+ inst = wrapped(context, request)
+ return inst()
+ _bfg_class_view.__module__ = wrapped.__module__
+ _bfg_class_view.__name__ = wrapped.__name__
+ _bfg_class_view.__doc__ = wrapped.__doc__
+ _bfg_view = _bfg_class_view
_bfg_view.__is_bfg_view__ = True
_bfg_view.__permission__ = self.permission
_bfg_view.__for__ = self.for_
_bfg_view.__view_name__ = self.name
_bfg_view.__request_type__ = self.request_type
- _bfg_view.__module__ = wrapped.__module__
- _bfg_view.__name__ = wrapped.__name__
- _bfg_view.__doc__ = wrapped.__doc__
- _bfg_view.__dict__.update(wrapped.__dict__)
return _bfg_view
diff --git a/repoze/bfg/zcml.py b/repoze/bfg/zcml.py
index 34b071676..737e2409b 100644
--- a/repoze/bfg/zcml.py
+++ b/repoze/bfg/zcml.py
@@ -1,3 +1,4 @@
+import inspect
import types
from zope.configuration import xmlconfig
@@ -50,7 +51,7 @@ def view(_context,
# adapts() decorations may be used against either functions or
# class instances
- if isinstance(view, types.FunctionType):
+ if inspect.isfunction(view):
adapted_by = adaptedBy(view)
else:
adapted_by = adaptedBy(type(view))
@@ -66,6 +67,22 @@ def view(_context,
# the view specification; we ignore it.
pass
+ if inspect.isclass(view):
+ # If the object we've located is a class, turn it into a
+ # function that operates like a Zope view (when it's invoked,
+ # construct an instance using 'context' and 'request' as
+ # position arguments, then immediately invoke the __call__
+ # method of the instance with no arguments; __call__ should
+ # return an IResponse).
+ _view = view
+ def _bfg_class_view(context, request):
+ inst = _view(context, request)
+ return inst()
+ _bfg_class_view.__module__ = view.__module__
+ _bfg_class_view.__name__ = view.__name__
+ _bfg_class_view.__doc__ = view.__doc__
+ view = _bfg_class_view
+
if request_type is None:
request_type = IRequest