summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt9
-rw-r--r--TODO.txt4
-rw-r--r--docs/glossary.rst7
-rw-r--r--docs/narr/events.rst31
-rw-r--r--docs/narr/traversal.rst12
-rw-r--r--docs/narr/views.rst139
-rw-r--r--repoze/bfg/request.py12
-rw-r--r--repoze/bfg/tests/test_zcml.py41
-rw-r--r--repoze/bfg/zcml.py18
9 files changed, 167 insertions, 106 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 988d2f433..d89e1b2cd 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,6 +1,15 @@
Next Release
============
+Features
+--------
+
+- The ``request_type`` argument of ZCML ``view`` declarations and
+ ``bfg_view`` decorators can now be one of the strings ``GET``,
+ ``POST``, ``PUT``, ``DELETE``, or ``HEAD`` instead of a reference to
+ the respective interface type imported from
+ ``repoze.bfg.interfaces``.
+
Removals
--------
diff --git a/TODO.txt b/TODO.txt
index 9bd3e6ec4..00e5c63a7 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -2,7 +2,5 @@
- audit use of "functools.wraps" (pickling no longer required)
-- accept "GET", "HEAD", "POST", etc as strings in request_type arg to
- view decorators and ZCML registrations.
+- SQLAlchemy + Routes tutorial (convert repoze.wiki)
-- SQLAlchemy + Routes tutorial.
diff --git a/docs/glossary.rst b/docs/glossary.rst
index e6efcc565..500338714 100644
--- a/docs/glossary.rst
+++ b/docs/glossary.rst
@@ -320,7 +320,12 @@ Glossary
that particular interface. Application code can cause requests to
possess a different interface by adding the interface to the
request object within a :term:`subscriber` to the
- ``repoze.bfg.interfaces.INewRequest`` event type.
+ ``repoze.bfg.interfaces.INewRequest`` event type. String aliases
+ such as ``GET``, ``POST``, etc. representing HTTP method names may
+ be used in place of an interface specification in the
+ ``request_type`` argument passed to view declarations. ``GET`` is
+ aliased to ``repoze.bfg.interfaces.IGETRequest``, ``POST`` is
+ aliased to ``repoze.bfg.interfaces.IPOSTRequest``, and so on.
repoze.lemonade
Zope2 CMF-like `data structures and helper facilities
<http://docs.repoze.org/lemonade>`_ for CA-and-ZODB-based
diff --git a/docs/narr/events.rst b/docs/narr/events.rst
index adb0ae7b0..e54c97a88 100644
--- a/docs/narr/events.rst
+++ b/docs/narr/events.rst
@@ -102,6 +102,8 @@ function.
The return value of a subscriber function is ignored.
+.. _using_an_event_to_vary_the_request_type:
+
Using An Event to Vary the Request Type
---------------------------------------
@@ -176,4 +178,31 @@ might also choose to use hostname
request.environ['SERVER_NAME'])``) in order to "skin" your application
differently based on whether the user should see the "management"
(e.g. "manage.myapp.com") presentation of the application or the
-"retail" presentation (e.g. "www.myapp.com").
+"retail" presentation (e.g. "www.myapp.com"). 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.
+
+:mod:`repoze.bfg` also makes available as interfaces standard request
+type interfaces matching HTTP verbs. When these are specified as a
+``request_type`` for a view, the view will be called only when the
+request has an HTTP verb (aka HTTP method) matching the request type.
+For example, using the string ``repoze.bfg.interfaces.IPOST`` or the
+imported interface definition itself matching that Python dotted name
+as the request_type argument to a view definition is equivalent to
+using the string ``POST``. See :ref:`interfaces_module` for more
+information about available request types.
diff --git a/docs/narr/traversal.rst b/docs/narr/traversal.rst
index 76823041a..7177432de 100644
--- a/docs/narr/traversal.rst
+++ b/docs/narr/traversal.rst
@@ -5,12 +5,12 @@ Traversal
When :term:`traversal` is used, the :mod:`repoze.bfg` *Router* parses
the URL associated with the request and splits the URL into path
-segments. Based on these path segments, :mod:`repoze.bfg` traverses
-a *model graph* in order to find a :term:`context`. It then
-attempts to find a :term:`view` based on the *type* of the context
-(specified by an :term:`interface`). If :mod:`repoze.bfg` finds a
-:term:`view` for the context, it calls it and returns a response to
-the user.
+segments. Based on these path segments, :mod:`repoze.bfg` traverses a
+*model graph* in order to find a :term:`context`. It then attempts to
+find a :term:`view` based on the *type* of the context (specified by
+its Python class type or any :term:`interface` attached to it). If
+:mod:`repoze.bfg` finds a :term:`view` for the context, it calls it
+and returns a response to the user.
The Model Graph
---------------
diff --git a/docs/narr/views.rst b/docs/narr/views.rst
index 9e9c55236..c55fc0937 100644
--- a/docs/narr/views.rst
+++ b/docs/narr/views.rst
@@ -208,10 +208,12 @@ permission
request_type
- A Python dotted-path name representing the :term:`interface` that
+ This value can either be one of the strings 'GET', 'POST', 'PUT',
+ 'DELETE', or 'HEAD' representing an HTTP method, *or* it may be
+ Python dotted-path string 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 request types.
+ called. See :ref:`view_request_types_section` for more information
+ about request types.
.. _mapping_views_to_urls_using_a_decorator_section:
@@ -256,11 +258,10 @@ An example might reside in a bfg application module ``views.py``:
:linenos:
from models import MyModel
- from repoze.bfg.interfaces import IPOSTRequest
from repoze.bfg.view import bfg_view
from repoze.bfg.chameleon_zpt import render_template_to_response
- @bfg_view(name='my_view', request_type=IPOSTRequest, for_=MyModel,
+ @bfg_view(name='my_view', request_type='POST', for_=MyModel,
permission='read')
def my_view(context, request):
return render_template_to_response('templates/my.pt')
@@ -276,7 +277,7 @@ your application registry:
view=".views.my_view"
name="my_view"
permission="read"
- request_type="repoze.bfg.interfaces.IPOSTRequest"
+ request_type="POST"
/>
All arguments to ``bfg_view`` are optional.
@@ -284,8 +285,8 @@ All arguments to ``bfg_view`` are optional.
If ``name`` is not supplied, the empty string is used (implying
the default view).
-If ``request_type`` is not supplied, the interface
-``repoze.bfg.interfaces.IRequest`` is used.
+If ``request_type`` is not supplied, the interface ``None`` is used,
+implying any request type.
If ``for_`` is not supplied, the interface
``zope.interface.Interface`` (which matches any model) is used.
@@ -307,10 +308,9 @@ All arguments may be omitted. For example:
return Response()
Such a registration as the one directly above implies that the view
-name will be ``my_view``, registered for models with the
-``zope.interface.Interface`` interface (which matches anything), using
-no permission, registered against requests which implement the default
-``IRequest`` interface.
+name will be ``my_view``, registered ``for_`` any model type, using no
+permission, registered against requests which implement any request
+method or 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
@@ -442,16 +442,22 @@ interfaces.
.. _view_request_types_section:
-View Request Types
-------------------
+Standard View Request Types
+---------------------------
You can optionally add a *request_type* attribute to your ``view``
-declaration, which indicates what "kind" of request the view should be
-used for. If the request type for a request doesn't match the request
-type that a view defines as its ``request_type`` argument, that view
-won't be called.
+declaration or ``bfg_view`` decorator, which indicates what "kind" of
+request the view should be used for. If the request type for a
+request doesn't match the request type that a view defines as its
+``request_type`` argument, that view won't be called.
-For example:
+The request type can be one of the strings 'GET', 'POST', 'PUT',
+'DELETE', or 'HEAD'. When the request type is one of these strings,
+the view will only be called when the HTTP method of a request matches
+this type.
+
+For example, the following bit of ZCML will match an HTTP POST
+request:
.. code-block:: xml
:linenos:
@@ -460,86 +466,45 @@ For example:
for=".models.Hello"
view=".views.handle_post"
name="handle_post"
- request_type="repoze.bfg.interfaces.IPOSTRequest"
+ request_type="POST"
/>
-The above example registers a view for the ``IPOSTRequest`` type, so
-it will only be called if the request is a POST request. Even if all
-the other specifiers match (e.g. the model type is the class
+A ``bfg_view`` decorator that does the same as the above ZCML ``view``
+declaration which matches only on HTTP POST might look something like:
+
+.. code-block:: python
+ :linenos:
+
+ from myproject.models import Hello
+ from webob import Response
+
+ @bfg_view(for=Hello, request_type='POST')
+ def handle_post(context, request):
+ return Response('hello'
+
+The above examples register views for the POST request type, so it
+will only be called if the request's HTTP method is ``POST``. Even if
+all the other specifiers match (e.g. the model type is the class
``.models.Hello``, and the view_name is ``handle_post``), if the
request verb is not POST, it will not be invoked. This provides a way
to ensure that views you write are only called via specific HTTP
verbs.
-The least specific request type is ``repoze.bfg.interfaces.IRequest``.
-All requests are guaranteed to implement this request type. It is
-also the default request type for views that omit a ``request_type``
-argument.
-
-:mod:`repoze.bfg` also makes available more specific request types
-matching HTTP verbs. When these are specified as a ``request_type``
-for a view, the view will be called only when the request has an HTTP
-verb (aka HTTP method) matching the request type. See
-:ref:`interfaces_module` for more information about available request
-types.
+The least specific request type is ``None``. All requests are
+guaranteed to implement this request type. It is also the default
+request type for views that omit a ``request_type`` argument.
Custom View Request Types
-------------------------
-You can make use of *custom* view request types. For example:
-
-.. code-block:: xml
- :linenos:
-
- <view
- for=".models.Hello"
- view=".views.hello_json"
- name="hello.json"
- request_type=".interfaces.IJSONRequest"
- />
-
-Where the code behind ``.interfaces.IJSONRequest`` might look like:
-
-.. code-block:: python
- :linenos:
+You can make use of *custom* view request types by attaching an
+:term:`interface` to the request and specifying this interface in the
+``request_type`` parameter. For example, you might want to make use
+of simple "content negotiation", only invoking a particular view if
+the request has a content-type of 'application/json'.
- 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.
-
-.. note:: see :ref:`events_chapter` for more information about event
- subscribers, and how to provide requests with differing request
- types.
+For information about using interface to specify a request type, see
+:ref:`using_an_event_to_vary_the_request_type`.
.. _view_security_section:
diff --git a/repoze/bfg/request.py b/repoze/bfg/request.py
index de5711e0a..44a232d0d 100644
--- a/repoze/bfg/request.py
+++ b/repoze/bfg/request.py
@@ -21,8 +21,8 @@ class Request(WebobRequest):
# an optimization (e.g. preventing calls to ``alsoProvides`` means the
# difference between 590 r/s and 690 r/s on a MacBook 2GHz). These
# classes are *not* APIs. None of these classes, nor the
-# ``HTTP_METHOD_FACTORIES`` lookup dict should be imported directly by
-# user code.
+# ``HTTP_METHOD_FACTORIES`` or ``HTTP_METHOD_INTERFACES`` lookup dicts
+# should be imported directly by user code.
class GETRequest(WebobRequest):
implements(repoze.bfg.interfaces.IGETRequest)
@@ -52,3 +52,11 @@ HTTP_METHOD_FACTORIES = {
'HEAD':HEADRequest,
}
+HTTP_METHOD_INTERFACES = {
+ 'GET':repoze.bfg.interfaces.IGETRequest,
+ 'POST':repoze.bfg.interfaces.IPOSTRequest,
+ 'PUT':repoze.bfg.interfaces.IPUTRequest,
+ 'DELETE':repoze.bfg.interfaces.IDELETERequest,
+ 'HEAD':repoze.bfg.interfaces.IHEADRequest,
+ }
+
diff --git a/repoze/bfg/tests/test_zcml.py b/repoze/bfg/tests/test_zcml.py
index e44fe1307..0b24071e2 100644
--- a/repoze/bfg/tests/test_zcml.py
+++ b/repoze/bfg/tests/test_zcml.py
@@ -109,7 +109,7 @@ class TestViewDirective(unittest.TestCase):
self.assertEqual(regadapt['args'][4], '')
self.assertEqual(regadapt['args'][5], None)
- def test_request_type(self):
+ def test_request_type_asinterface(self):
context = DummyContext()
class IFoo:
pass
@@ -148,6 +148,42 @@ class TestViewDirective(unittest.TestCase):
self.assertEqual(regadapt['args'][4], '')
self.assertEqual(regadapt['args'][5], None)
+ def test_request_type_ashttpmethod(self):
+ context = DummyContext()
+ class IFoo:
+ pass
+ view = lambda *arg: None
+ self._callFUT(context, 'repoze.view', IFoo, view=view,
+ request_type='GET')
+ actions = context.actions
+ from repoze.bfg.interfaces import IView
+ from repoze.bfg.interfaces import IGETRequest
+
+ self.assertEqual(len(actions), 2)
+
+ permission = actions[0]
+ self.assertEqual(permission['args'][2], (IFoo, IGETRequest))
+ regadapt = actions[1]
+ regadapt_discriminator = ('view', IFoo, '', IDummy, IView)
+ self.assertEqual(regadapt['args'][2], (IFoo, IGETRequest))
+
+ def test_request_type_asinterfacestring(self):
+ context = DummyContext()
+ class IFoo:
+ pass
+ view = lambda *arg: None
+ self._callFUT(context, 'repoze.view', IFoo, view=view,
+ request_type='whatever')
+ actions = context.actions
+ from repoze.bfg.interfaces import IView
+ self.assertEqual(len(actions), 2)
+
+ permission = actions[0]
+ self.assertEqual(permission['args'][2], (IFoo, IDummy))
+ regadapt = actions[1]
+ regadapt_discriminator = ('view', IFoo, '', IDummy, IView)
+ self.assertEqual(regadapt['args'][2], (IFoo, IDummy))
+
def test_adapted_class(self):
from zope.interface import Interface
import zope.component
@@ -633,6 +669,9 @@ class DummyContext:
'args':args}
)
+ def resolve(self, dottedname):
+ return IDummy
+
class Dummy:
pass
diff --git a/repoze/bfg/zcml.py b/repoze/bfg/zcml.py
index 39366b0b1..0f9d4a22b 100644
--- a/repoze/bfg/zcml.py
+++ b/repoze/bfg/zcml.py
@@ -25,6 +25,8 @@ from repoze.bfg.interfaces import IRoutesContext
from repoze.bfg.interfaces import IViewPermission
from repoze.bfg.interfaces import IView
+from repoze.bfg.request import HTTP_METHOD_INTERFACES
+
from repoze.bfg.security import ViewPermissionFactory
import martian
@@ -68,6 +70,15 @@ def view(
# the view specification; we ignore it.
pass
+ if request_type is None:
+ request_type = IRequest
+
+ elif isinstance(request_type, basestring):
+ if request_type in HTTP_METHOD_INTERFACES:
+ request_type = HTTP_METHOD_INTERFACES[request_type]
+ else:
+ request_type = _context.resolve(request_type)
+
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,
@@ -84,9 +95,6 @@ def view(
_bfg_class_view.__doc__ = view.__doc__
view = _bfg_class_view
- if request_type is None:
- request_type = IRequest
-
if permission:
pfactory = ViewPermissionFactory(permission)
_context.action(
@@ -132,8 +140,8 @@ class IViewDirective(Interface):
required=False,
)
- request_type = GlobalObject(
- title=u"""The request type interface for the view""",
+ request_type = TextLine(
+ title=u"The request type string or dotted name 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"),