diff options
| -rw-r--r-- | CHANGES.txt | 11 | ||||
| -rw-r--r-- | docs/api/events.rst | 39 | ||||
| -rw-r--r-- | docs/index.rst | 1 | ||||
| -rw-r--r-- | docs/narr/events.rst | 119 | ||||
| -rw-r--r-- | docs/narr/views.rst | 172 | ||||
| -rw-r--r-- | docs/notes.txt | 16 | ||||
| -rw-r--r-- | repoze/bfg/tests/fixtureapp/configure.zcml | 7 | ||||
| -rw-r--r-- | repoze/bfg/tests/fixtureapp/views.py | 5 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_zcml.py | 108 | ||||
| -rw-r--r-- | repoze/bfg/zcml.py | 61 |
10 files changed, 334 insertions, 205 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 111be6294..a76a91bc0 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,14 @@ +After 0.2.6 + + - Add a ``request_type`` attribute to the available attributes of a + ``bfg:view`` configure.zcml element. This attribute will have a + value which is a dotted Python path, pointing at an interface. If + the request object implements this interface when the view lookup + is performed, the appropriate view will be called. + + - Remove "template only" views. These were just confusing and were + never documented. + 0.2.6 - Add event sends for INewRequest and INewResponse. See the diff --git a/docs/api/events.rst b/docs/api/events.rst index 25bb9841b..b2cd4a100 100644 --- a/docs/api/events.rst +++ b/docs/api/events.rst @@ -9,41 +9,6 @@ .. autoclass:: NewResponse -You can write *listeners* for these event types and subsequently -register the listeners to be called when the events occur. For -example, if you create event listener functions in a ``listeners.py`` -file in your application like so: - -.. code-block:: python - :linenos: - - def handle_new_request(event): - print 'request', event.request - - def handle_new_response(event): - print 'response', event.response - -You may configure these functions to be called at the appropriate -times by adding the following to your application's ``configure.zcml`` -file: - -.. code-block:: xml - :linenos: - - <subscriber - for="repoze.bfg.interfaces.INewRequest" - handler=".listeners.handle_new_request" - /> - - <subscriber - for="repoze.bfg.interfaces.INewResponse" - handler=".listeners.handle_new_response" - /> - -This causes the functions as to be registered as event listeners -within the :term:`application registry` . Under this configuration, -when the application is run, every new request and every response will -be printed to the console. - -The return value of a listener function is ignored. +See :ref:`events_chapter` for more information about how to register +code which subscribes to these events. diff --git a/docs/index.rst b/docs/index.rst index 3f20d0797..a11ebfe45 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -32,6 +32,7 @@ Narrative documentation in chapter form explaining how to use narr/templates narr/models narr/security + narr/events glossary Tutorials diff --git a/docs/narr/events.rst b/docs/narr/events.rst new file mode 100644 index 000000000..1e69a6f7f --- /dev/null +++ b/docs/narr/events.rst @@ -0,0 +1,119 @@ +.. _events_chapter: + +Using Events +============= + +An *event* is an object broadcast by the :mod:`repoze.bfg` framework +at particularly interesting points during the lifetime of your +application. You don't need to use, know about, or care about events +in order to create most :mod:`repoze.bfg` applications, but they can +be useful when you want to do slightly advanced operations, such as +"skinning" a site slightly differently based on, for example, the +hostname used to reach the site. + +Events in :mod:`repoze.bfg` are always broadcast by the framework. +They only become useful when you register a *subscriber*. A +subscriber is a function that accepts a single argument named `event`: + +.. code-block:: python + :linenos: + + def mysubscriber(event): + print event + +The above is a subscriber that simply prints the event to the console +when it's called. + +The mere existence of a subscriber function, however, is not +sufficient to arrange for it to be called. To arrange for the +subscriber to be called, you'll need to change your :term:`application +registry` by modifying your application's ``configure.zcml``. Here's +an example of a bit of XML you can add to the ``configure.zcml`` file +which registers the above ``mysubscriber`` function, which we assume +lives in a ``subscribers.py`` module within your application: + +.. code-block:: xml + :linenos: + + <subscriber + for="repoze.bfg.interfaces.INewRequest" + handler=".subscribers.mysubscriber" + /> + +The above example means "every time the :mod:`repoze.bfg` framework +emits an event object that supplies an ``INewRequest`` interface, call +the ``mysubscriber`` function with the event object. As you can see, +subscriptions are made to *interfaces*. The event object sent to a +subscriber will always have an interface. You can use the interface +itself to determine what attributes of the event are available. + +For example, if you create event listener functions in a +``subscribers.py`` file in your application like so: + +.. code-block:: python + :linenos: + + def handle_new_request(event): + print 'request', event.request + + def handle_new_response(event): + print 'response', event.response + +You may configure these functions to be called at the appropriate +times by adding the following to your application's ``configure.zcml`` +file: + +.. code-block:: xml + :linenos: + + <subscriber + for="repoze.bfg.interfaces.INewRequest" + handler=".subscribers.handle_new_request" + /> + + <subscriber + for="repoze.bfg.interfaces.INewResponse" + handler=".subscribers.handle_new_response" + /> + +This causes the functions as to be registered as event subscribers +within the :term:`application registry` . Under this configuration, +when the application is run, every new request and every response will +be printed to the console. We know that ``INewRequest`` events have a +``request`` attribute, which is a :term:`WebOb` request, because the +interface defined at ``repoze.bfg.interfaces.INewRequest`` says it +must. Likewise, we know that ``INewResponse`` events have a +``response`` attribute, which is a response object constructed by your +application, because the interface defined at +``repoze.bfg.interfaces.INewResponse`` says it must. These particular +interfaces are documented in the :ref:`events_module` API chapter. + +The *subscriber* ZCML element takes two values: ``for``, which is the +interface the subscriber is registered for (which limits the events +that the subscriber will receive to those specified by the interface), +and ``handler`` which is a Python dotted-name path to the subscriber +function. + +The return value of a subscriber function is ignored. + +Uses For Events +--------------- + +Here are some things that events are useful for: + +- Attaching different interfaces to the request to be able to + differentiate e.g. requests from a browser against requests from an + XML-RPC client within view code. To do this, you'd subscribe a + function to ```INewRequest``, and use the + ``zope.interface.alsoProvides`` function to add one or more + interfaces to the request object. + +- Post-processing all response output by subscribing to + ``INewResponse``, for example, modifying headers. + + .. note:: + + Usually postprocessing requests is better handled in middleware + components. The ``INewResponse`` event exists purely for + symmetry with ``INewRequest``, really. + diff --git a/docs/narr/views.rst b/docs/narr/views.rst index 33e80caf7..36273de9d 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -1,3 +1,5 @@ +.. _views_chapter: + Views ===== @@ -11,11 +13,14 @@ Defining a View as a Function The easiest way to define a view is to create a function that accepts two arguments: :term:`context`, and :term:`request`. For example, -this is a hello world view implemented as a function:: +this is a hello world view implemented as a function: + +.. code-block:: python + :linenos: - def hello_world(context, request): - from webob import Response - return Response('Hello world!') + 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: @@ -63,13 +68,14 @@ You must associate a view with a URL by adding information to your :term:`application registry` via :term:`ZCML` in your ``configure.zcml`` file using a ``bfg:view`` declaration. -.. sourcecode:: xml +.. code-block:: xml + :linenos: - <bfg:view - for=".models.IHello" - view=".views.hello_world" - name="hello.html" - /> + <bfg:view + for=".models.IHello" + view=".views.hello_world" + name="hello.html" + /> The above maps the ``.views.hello_world`` view function to :term:`context` objects which implement the ``.models.IHello`` @@ -89,12 +95,13 @@ changes. It's also shorter to type. You can also declare a *default view* for a model type: -.. sourcecode:: xml +.. code-block:: xml + :linenos: - <bfg:view - for=".models.IHello" - view=".views.hello_world" - /> + <bfg:view + for=".models.IHello" + view=".views.hello_world" + /> A *default view* has no ``name`` attribute. When a :term:`context` is traversed and there is no *view name* in the request, the *default @@ -103,18 +110,115 @@ view* is the view that is used. You can also declare that a view is good for any model type by using the special ``*`` character in the ``for`` attribute: -.. sourcecode:: xml +.. code-block:: xml + :linenos: - <bfg:view - for="*" - view=".views.hello_world" - name="hello.html" - /> + <bfg:view + for="*" + view=".views.hello_world" + name="hello.html" + /> This indicates that when :mod:`repoze.bfg` identifies that the *view name* is ``hello.html`` against *any* :term:`context`, this view will be called. +The ``bfg:view`` ZCML Element +----------------------------- + +The ``bfg:view`` ZCML element has these possible attributes: + +view + + The Python dotted-path name to the view callable. + +for + + A Python dotted-path name representing the :term:`interface` that + the :term:`context` must have in order for this view to be found and + called. + +name + + The *view name*. Read and understand :ref:`traversal_chapter` to + understand the concept of a view name. + +permission + + The name of a *permission* that the user must possess in order to + call the view. See :ref:`view_security_section` for more + information about view security and permissions. + +request_type + + A Python dotted-path name representing the :term:`interface` that + the :term:`request` must have in order for this view to be found and + called. See :ref:`view_request_types_section` for more + information about view security and permissions. + +.. _view_request_types_section: + +View Request Types +------------------ + +You can optionally add a *request_type* attribute to your ``bfg:view`` +declaration, which indicates what "kind" of request the view should be +used for. For example: + +.. code-block:: xml + :linenos: + + <bfg:view + for=".models.IHello" + view=".views.hello_json" + name="hello.json" + request_type=".interfaces.IJSONRequest" + /> + +Where the code behind ``.interfaces.IJSONRequest`` might look like: + +.. code-block:: python + :linenos: + + from repoze.bfg.interfaces import IRequest + + class IJSONRequest(IRequest): + """ An marker interface for representing a JSON request """ + +This is an example of simple "content negotiation", using JSON as an +example. To make sure that this view will be called when the request +comes from a JSON client, you can use an ``INewRequest`` event +subscriber to attach the ``IJSONRequest`` interface to the request if +and only if the request headers indicate that the request has come +from a JSON client. Since we've indicated that the ``request_type`` +in our ZCML for this particular view is ``.interfaces.IJSONRequest``, +the view will only be called if the request provides this interface. + +You can also use this facility for "skinning" a by using request +parameters to vary the interface(s) that a request provides. By +attaching to the request an arbitrary interface after examining the +hostname or any other information available in the request within an +``INewRequest`` event subscriber, you can control view lookup +precisely. For example, if you wanted to have two slightly different +views for requests to two different hostnames, you might register one +view with a ``request_type`` of ``.interfaces.IHostnameFoo`` and +another with a ``request_type`` of ``.interfaces.IHostnameBar`` and +then arrange for an event subscriber to attach +``.interfaces.IHostnameFoo`` to the request when the HTTP_HOST is +``foo`` and ``.interfaces.IHostnameBar`` to the request when the +HTTP_HOST is ``bar``. The appropriate view will be called. + +You can also form an inheritance hierarchy out of ``request_type`` +interfaces. When :mod:`repoze.bfg` looks up a view, the most specific +view for the interface(s) found on the request based on standard +Python method resolution order through the interface class hierarchy +will be called. + +See :ref:`events_chapter` for more information about event +subscribers. + +.. _view_security_section: + View Security ------------- @@ -125,16 +229,24 @@ against the context before the view function is actually called. Here's an example of specifying a permission in a ``bfg:view`` declaration: -.. sourcecode:: xml +.. code-block:: xml + :linenos: - <bfg:view - for=".models.IBlog" - view=".views.add_entry" - name="add.html" - permission="add" - /> + <bfg:view + for=".models.IBlog" + view=".views.add_entry" + name="add.html" + permission="add" + /> When a security policy is enabled, this view will be protected with -the ``add`` permission. See the :ref:`security_chapter` chapter to -find out how to turn on a security policy. +the ``add`` permission. The view will not be called if the user does +not possess the ``add`` permission relative to the current +:term:`context`. Instead an HTTP ``Unauthorized`` status will be +returned to the client. + +.. note:: + + See the :ref:`security_chapter` chapter to find out how to turn on + a security policy. diff --git a/docs/notes.txt b/docs/notes.txt new file mode 100644 index 000000000..e8d679237 --- /dev/null +++ b/docs/notes.txt @@ -0,0 +1,16 @@ +- Document z3c.pt + +- Spaces in project names (allow for separate project / package names?) + +- Subpath and view name in request + +- WebOb Request basics + +- "push" style templating + +- .001 case where there is a template without a view. + +- Warn if permissions are defined but no security policy is in place. + +- Change port num due to conflict with Postgres. + diff --git a/repoze/bfg/tests/fixtureapp/configure.zcml b/repoze/bfg/tests/fixtureapp/configure.zcml index 265d69511..dfedda7bb 100644 --- a/repoze/bfg/tests/fixtureapp/configure.zcml +++ b/repoze/bfg/tests/fixtureapp/configure.zcml @@ -5,16 +5,17 @@ <include package="repoze.bfg" /> <bfg:view - for=".models.IFixture" view=".views.fixture_view" + for=".models.IFixture" permission="repoze.view" /> <bfg:view + view=".views.fixture_view" for=".models.IFixture" - template="templates/fixture.pt" + name="dummyskin.html" permission="repoze.view" - name="fixture.html" + request_type=".views.IDummy" /> </configure> diff --git a/repoze/bfg/tests/fixtureapp/views.py b/repoze/bfg/tests/fixtureapp/views.py index 2babbc59c..ccf0e4811 100644 --- a/repoze/bfg/tests/fixtureapp/views.py +++ b/repoze/bfg/tests/fixtureapp/views.py @@ -1,3 +1,8 @@ +from zope.interface import Interface + def fixture_view(context, request): return None +class IDummy(Interface): + pass + diff --git a/repoze/bfg/tests/test_zcml.py b/repoze/bfg/tests/test_zcml.py index f8ef78ff8..663022796 100644 --- a/repoze/bfg/tests/test_zcml.py +++ b/repoze/bfg/tests/test_zcml.py @@ -13,54 +13,37 @@ class TestViewDirective(unittest.TestCase, PlacelessSetup): from repoze.bfg.zcml import view return view - def test_no_class_or_template(self): + def test_no_view(self): f = self._getFUT() from zope.configuration.exceptions import ConfigurationError context = DummyContext() self.assertRaises(ConfigurationError, f, context, 'repoze.view', None) - def test_no_such_file(self): - f = self._getFUT() - from zope.configuration.exceptions import ConfigurationError - context = DummyContext() - self.assertRaises(ConfigurationError, f, context, 'repoze.view', None, - template='notthere.pt') - - def test_only_template(self): + def test_only_view(self): f = self._getFUT() context = DummyContext() class IFoo: pass - f(context, 'repoze.view', IFoo, template='minimal.pt') + def view(context, request): + pass + f(context, 'repoze.view', IFoo, view=view) actions = context.actions - from repoze.bfg.interfaces import ITemplate - from repoze.bfg.interfaces import IView 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 zope.component.zcml import handler from zope.component.interface import provideInterface - self.assertEqual(len(actions), 4) - - regutil_discriminator = ('utility', ITemplate, - context.path('minimal.pt')) - regutil = actions[0] - self.assertEqual(regutil['discriminator'], regutil_discriminator) - self.assertEqual(regutil['callable'], handler) - self.assertEqual(regutil['args'][0], 'registerUtility') - self.assertEqual(regutil['args'][1].template.filename, - context.path('minimal.pt')) - self.assertEqual(regutil['args'][2], ITemplate) - self.assertEqual(regutil['args'][3], context.path('minimal.pt')) + self.assertEqual(len(actions), 3) - provide = actions[1] + provide = actions[0] self.assertEqual(provide['discriminator'], None) self.assertEqual(provide['callable'], provideInterface) self.assertEqual(provide['args'][0], '') self.assertEqual(provide['args'][1], IFoo) - - permission = actions[2] + + permission = actions[1] permission_discriminator = ('permission', IFoo, '', IRequest, IViewPermission) self.assertEqual(permission['discriminator'], permission_discriminator) @@ -72,29 +55,27 @@ class TestViewDirective(unittest.TestCase, PlacelessSetup): self.assertEqual(permission['args'][3], IViewPermission) self.assertEqual(permission['args'][4], '') self.assertEqual(permission['args'][5], None) - - regadapt = actions[3] + + regadapt = actions[2] regadapt_discriminator = ('view', IFoo, '', IRequest, IView) self.assertEqual(regadapt['discriminator'], regadapt_discriminator) self.assertEqual(regadapt['callable'], handler) self.assertEqual(regadapt['args'][0], 'registerAdapter') - self.assertEqual(regadapt['args'][1].template, - context.path('minimal.pt')) + self.assertEqual(regadapt['args'][1], view) 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_only_factory(self): + def test_request_type(self): f = self._getFUT() context = DummyContext() class IFoo: pass def view(context, request): pass - f(context, 'repoze.view', IFoo, view=view) + f(context, 'repoze.view', IFoo, view=view, request_type=IDummy) 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 @@ -110,77 +91,29 @@ class TestViewDirective(unittest.TestCase, PlacelessSetup): self.assertEqual(provide['args'][1], IFoo) permission = actions[1] - permission_discriminator = ('permission', IFoo, '', IRequest, + permission_discriminator = ('permission', IFoo, '', IDummy, 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'][2], (IFoo, IDummy)) self.assertEqual(permission['args'][3], IViewPermission) self.assertEqual(permission['args'][4], '') self.assertEqual(permission['args'][5], None) regadapt = actions[2] - regadapt_discriminator = ('view', IFoo, '', IRequest, IView) + regadapt_discriminator = ('view', IFoo, '', IDummy, IView) self.assertEqual(regadapt['discriminator'], regadapt_discriminator) self.assertEqual(regadapt['callable'], handler) self.assertEqual(regadapt['args'][0], 'registerAdapter') self.assertEqual(regadapt['args'][1], view) - self.assertEqual(regadapt['args'][2], (IFoo, IRequest)) + self.assertEqual(regadapt['args'][2], (IFoo, IDummy)) self.assertEqual(regadapt['args'][3], IView) self.assertEqual(regadapt['args'][4], '') self.assertEqual(regadapt['args'][5], None) - def test_template_and_factory(self): - f = self._getFUT() - context = DummyContext() - from zope.configuration.exceptions import ConfigurationError - self.assertRaises(ConfigurationError, f, context, 'repoze.view', - None, view=object, template='minimal.pt') - -class TemplateOnlyViewFactoryTests(unittest.TestCase, PlacelessSetup): - def setUp(self): - PlacelessSetup.setUp(self) - - def tearDown(self): - PlacelessSetup.tearDown(self) - - def _getTargetClass(self): - from repoze.bfg.zcml import TemplateOnlyViewFactory - return TemplateOnlyViewFactory - - def _zcmlConfigure(self): - import repoze.bfg - import zope.configuration.xmlconfig - zope.configuration.xmlconfig.file('configure.zcml', package=repoze.bfg) - - def _getTemplatePath(self, name): - import os - here = os.path.abspath(os.path.dirname(__file__)) - return os.path.join(here, 'fixtures', name) - - def _makeOne(self, *arg, **kw): - klass = self._getTargetClass() - return klass(*arg, **kw) - - def test_call(self): - self._zcmlConfigure() - path = self._getTemplatePath('minimal.pt') - view = self._makeOne(path) - result = view(None, None) - from webob import Response - self.failUnless(isinstance(result, Response)) - self.assertEqual(result.app_iter, ['<div>\n</div>']) - self.assertEqual(result.status, '200 OK') - self.assertEqual(len(result.headerlist), 2) - - def test_call_no_template(self): - self._zcmlConfigure() - view = self._makeOne('nosuch') - self.assertRaises(ValueError, view, None, None) - class TestSampleApp(unittest.TestCase, PlacelessSetup): def setUp(self): PlacelessSetup.setUp(self) @@ -224,5 +157,8 @@ class DummyContext: 'args':args} ) +from zope.interface import Interface +class IDummy(Interface): + pass diff --git a/repoze/bfg/zcml.py b/repoze/bfg/zcml.py index 7d81527d2..343abc1d7 100644 --- a/repoze/bfg/zcml.py +++ b/repoze/bfg/zcml.py @@ -1,68 +1,28 @@ -import os - from zope.component.zcml import handler from zope.component.interface import provideInterface from zope.configuration.exceptions import ConfigurationError from zope.configuration.fields import GlobalObject -from zope.configuration.fields import Path from zope.interface import Interface -from zope.interface import implements -from zope.interface import classProvides from zope.schema import TextLine from repoze.bfg.interfaces import IRequest -from repoze.bfg.interfaces import ITemplateFactory -from repoze.bfg.interfaces import ITemplate from repoze.bfg.interfaces import IViewPermission from repoze.bfg.interfaces import IView -from repoze.bfg.template import Z3CPTTemplateFactory -from repoze.bfg.template import render_template_to_response - from repoze.bfg.security import ViewPermissionFactory -class TemplateOnlyViewFactory(object): - """ Pickleable template-only view factory """ - classProvides(ITemplateFactory) - implements(IView) - - def __init__(self, template): - self.template = template - - def __call__(self, context, request): - kw = dict(view=self, context=context, request=request) - return render_template_to_response(self.template, **kw) - def view(_context, permission=None, for_=None, view=None, name="", - template=None, + request_type=IRequest, ): - if (template and view): - raise ConfigurationError( - 'One of template or view must be specified, not both') - - if template: - template_abs = os.path.abspath(str(_context.path(template))) - if not os.path.exists(template_abs): - raise ConfigurationError('No template file named %s' % template_abs) - utility = Z3CPTTemplateFactory(template_abs) - _context.action( - discriminator = ('utility', ITemplate, template_abs), - callable = handler, - args = ('registerUtility', utility, ITemplate, template_abs), - ) - view = TemplateOnlyViewFactory(template_abs) - if not view: - raise ConfigurationError( - 'Neither template nor factory was specified, though one must be ' - 'specified.') + raise ConfigurationError('"view" attribute was not specified') if for_ is not None: _context.action( @@ -74,18 +34,19 @@ def view(_context, if permission: pfactory = ViewPermissionFactory(permission) _context.action( - discriminator = ('permission', for_,name, IRequest,IViewPermission), + discriminator = ('permission', for_,name, request_type, + IViewPermission), callable = handler, args = ('registerAdapter', - pfactory, (for_, IRequest), IViewPermission, name, + pfactory, (for_, request_type), IViewPermission, name, _context.info), ) _context.action( - discriminator = ('view', for_, name, IRequest, IView), + discriminator = ('view', for_, name, request_type, IView), callable = handler, args = ('registerAdapter', - view, (for_, IRequest), IView, name, + view, (for_, request_type), IView, name, _context.info), ) @@ -115,9 +76,11 @@ class IViewDirective(Interface): required=False, ) - template = Path( - title=u"The name of a template that implements the view.", - description=u"""Refers to a file containing a z3c.pt page template""", + request_type = GlobalObject( + title=u"""The request type interface for the view""", + description=(u"The view will be called if the interface represented by " + u"'request_type' is implemented by the request. The " + u"default request type is repoze.bfg.interfaces.IRequest"), required=False ) |
