diff options
| author | Chris McDonough <chrism@agendaless.com> | 2009-05-21 16:01:58 +0000 |
|---|---|---|
| committer | Chris McDonough <chrism@agendaless.com> | 2009-05-21 16:01:58 +0000 |
| commit | 5a11e2ad0828b7c763d0c81211f686a85bc0324c (patch) | |
| tree | 750deaa5086279a1cd0baa28c0d5bdaa17414463 | |
| parent | 385084582eeff5f2f1a93f3b90c091dc1a4ad50e (diff) | |
| download | pyramid-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.txt | 22 | ||||
| -rw-r--r-- | docs/narr/views.rst | 107 | ||||
| -rw-r--r-- | repoze/bfg/tests/grokkedapp/__init__.py | 15 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_integration.py | 12 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_view.py | 54 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_zcml.py | 53 | ||||
| -rw-r--r-- | repoze/bfg/view.py | 59 | ||||
| -rw-r--r-- | repoze/bfg/zcml.py | 19 |
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 |
