diff options
| author | Chris McDonough <chrism@agendaless.com> | 2009-05-31 20:36:53 +0000 |
|---|---|---|
| committer | Chris McDonough <chrism@agendaless.com> | 2009-05-31 20:36:53 +0000 |
| commit | 6c7b9a1534d35aeb07f4a9a59b8e15633f6b6b6b (patch) | |
| tree | 184f1553b041226cac7e8a83b71578e556e15874 | |
| parent | 8c3b807625d0480c7ce934bf7daf3437328346bf (diff) | |
| download | pyramid-6c7b9a1534d35aeb07f4a9a59b8e15633f6b6b6b.tar.gz pyramid-6c7b9a1534d35aeb07f4a9a59b8e15633f6b6b6b.tar.bz2 pyramid-6c7b9a1534d35aeb07f4a9a59b8e15633f6b6b6b.zip | |
- 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``.
| -rw-r--r-- | CHANGES.txt | 9 | ||||
| -rw-r--r-- | TODO.txt | 4 | ||||
| -rw-r--r-- | docs/glossary.rst | 7 | ||||
| -rw-r--r-- | docs/narr/events.rst | 31 | ||||
| -rw-r--r-- | docs/narr/traversal.rst | 12 | ||||
| -rw-r--r-- | docs/narr/views.rst | 139 | ||||
| -rw-r--r-- | repoze/bfg/request.py | 12 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_zcml.py | 41 | ||||
| -rw-r--r-- | repoze/bfg/zcml.py | 18 |
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 -------- @@ -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"), |
