[Checkins] SVN: Zope3/trunk/ Introduced IResult hook for postprocessing of publishing results. This

Gary Poster gary at zope.com
Tue Apr 17 17:45:59 EDT 2007


Log message for revision 74219:
  Introduced IResult hook for postprocessing of publishing results. This
  replaces a similar private IResult hook from previous releases. It is a
  hook point into which a variety of interesting policies, including
  in-Zope pipelining, can be placed.  See zope/publisher/httpresults.txt
  for more details, which itself points to the full details for IResult
  and IHTTPResponse.setResult in zope/publisher/interfaces/http.py.
  
  

Changed:
  U   Zope3/trunk/doc/CHANGES.txt
  U   Zope3/trunk/src/zope/app/wsgi/README.txt
  U   Zope3/trunk/src/zope/app/wsgi/fileresult.py
  U   Zope3/trunk/src/zope/app/wsgi/fileresult.txt
  U   Zope3/trunk/src/zope/publisher/http.py
  U   Zope3/trunk/src/zope/publisher/httpresults.txt
  U   Zope3/trunk/src/zope/publisher/interfaces/http.py
  U   Zope3/trunk/src/zope/publisher/tests/test_http.py
  U   Zope3/trunk/src/zope/publisher/xmlrpc.py

-=-
Modified: Zope3/trunk/doc/CHANGES.txt
===================================================================
--- Zope3/trunk/doc/CHANGES.txt	2007-04-17 21:44:47 UTC (rev 74218)
+++ Zope3/trunk/doc/CHANGES.txt	2007-04-17 21:45:57 UTC (rev 74219)
@@ -10,6 +10,14 @@
 
     New features
 
+      - Introduced IResult hook for postprocessing of publishing results.
+        This replaces a similar private IResult hook from previous releases.
+        It is a hook point into which a variety of interesting policies,
+        including in-Zope pipelining, can be placed.  See 
+        zope/publisher/httpresults.txt for more details, which itself
+        points to the full details for IResult and IHTTPResponse.setResult in 
+        zope/publisher/interfaces/http.py.
+
       - Implemented a custom FieldStorage that uses named temporary files to
         allow the Blob integration to use `consumeFile` on uploaded files.
 
@@ -398,7 +406,8 @@
     Much thanks to everyone who contributed to this release:
 
       Jim Fulton, Dmitry Vasiliev, Martijn Faassen, Christian Theune, Wolfgang
-      Schnerring, Fred Drake, Marius Gedminas, Baiju M, Brian Sutherland
+      Schnerring, Fred Drake, Marius Gedminas, Baiju M, Brian Sutherland,
+      Gary Poster
 
   ------------------------------------------------------------------
 

Modified: Zope3/trunk/src/zope/app/wsgi/README.txt
===================================================================
--- Zope3/trunk/src/zope/app/wsgi/README.txt	2007-04-17 21:44:47 UTC (rev 74218)
+++ Zope3/trunk/src/zope/app/wsgi/README.txt	2007-04-17 21:45:57 UTC (rev 74219)
@@ -135,9 +135,9 @@
 About WSGI
 ----------
 
-WSGI is the Python Web Server Gateway Interface, an upcoming PEP to
-standardize the interface between web servers and python applications to
-promote portability.
+WSGI is the Python Web Server Gateway Interface, a PEP to standardize
+the interface between web servers and python applications to promote
+portability.
 
 For more information, refer to the WSGI specification:
 http://www.python.org/peps/pep-0333.html

Modified: Zope3/trunk/src/zope/app/wsgi/fileresult.py
===================================================================
--- Zope3/trunk/src/zope/app/wsgi/fileresult.py	2007-04-17 21:44:47 UTC (rev 74218)
+++ Zope3/trunk/src/zope/app/wsgi/fileresult.py	2007-04-17 21:45:57 UTC (rev 74219)
@@ -20,10 +20,11 @@
 from zope import component, interface
 import zope.publisher.interfaces.http
 import zope.publisher.http
-from zope.publisher.http import DirectResult
+from zope.publisher.interfaces.http import IResult
 from zope.security.proxy import removeSecurityProxy
 
 class FallbackWrapper:
+    interface.implements(IResult)
 
     def __init__(self, f):
         self.close = f.close
@@ -42,19 +43,18 @@
 @interface.implementer(zope.publisher.http.IResult)
 def FileResult(f, request):
     f = removeSecurityProxy(f)
-    headers = ()
     if request.response.getHeader('content-length') is None:
         f.seek(0, 2)
         size = f.tell()
         f.seek(0)
-        headers += (('Content-Length', str(size)), )
+        request.response.setHeader('Content-Length', str(size))
         
     wrapper = request.environment.get('wsgi.file_wrapper')
     if wrapper is not None:
         f = wrapper(f)
     else:
         f = FallbackWrapper(f)
-    return DirectResult(f, headers)
+    return f
 
 # We need to provide an adapter for temporary files *if* they are different
 # than regular files. Whether they are is system dependent. Sigh.

Modified: Zope3/trunk/src/zope/app/wsgi/fileresult.txt
===================================================================
--- Zope3/trunk/src/zope/app/wsgi/fileresult.txt	2007-04-17 21:44:47 UTC (rev 74218)
+++ Zope3/trunk/src/zope/app/wsgi/fileresult.txt	2007-04-17 21:45:57 UTC (rev 74219)
@@ -1,12 +1,12 @@
 File results
 ============
 
-The file results adapters provide adapters from Python file objects
-to and from temporary file objects to zope.publisher.http.IResult. They also have
-the property that they can handle security proxied files and unproxy
-them in the result. Finally, if the request has a wsgi.file_wrapper
-environment variable, then that is used to wrap the file in the
-result.
+The file results adapters provide adapters from Python file objects to
+and from temporary file objects to zope.publisher.interfaces.http.IResult.
+They also have the property that they can handle security proxied files
+and unproxy them in the result. Finally, if the request has a
+wsgi.file_wrapper environment variable, then that is used to wrap the
+file in the result.
 
 Lets look at an example with a regular file object:
 
@@ -21,35 +21,36 @@
     >>> f = open(os.path.join(dir, 'f'), 'w+b')
     >>> f.write('One\nTwo\nThree\nHa ha! I love to count!\n')
     >>> from zope.security.checker import ProxyFactory
-    >>> from zope.publisher.http import IResult
+    >>> from zope.publisher.interfaces.http import IResult
     >>> from zope.publisher.browser import TestRequest
     >>> request = TestRequest()
     >>> result = component.getMultiAdapter((ProxyFactory(f), request), IResult)
-    >>> for line in result.body:
+    >>> for line in result:
     ...     print line
     One
     Two
     Three
     Ha ha! I love to count!
 
-    >>> result.headers
-    (('Content-Length', '38'),)
+    >>> request.response.getHeader('content-length')
+    '38'
     
 
 We'll see something similar with a temporary file:
 
     >>> t = tempfile.TemporaryFile()
     >>> t.write('Three\nTwo\nOne\nHa ha! I love to count down!\n')
+    >>> request = TestRequest()
     >>> result = component.getMultiAdapter((ProxyFactory(t), request), IResult)
-    >>> for line in result.body:
+    >>> for line in result:
     ...     print line
     Three
     Two
     One
     Ha ha! I love to count down!
 
-    >>> result.headers
-    (('Content-Length', '43'),)
+    >>> request.response.getHeader('content-length')
+    '43'
 
         
 If we provide a custom file wrapper:
@@ -60,22 +61,23 @@
  
     >>> request = TestRequest(environ={'wsgi.file_wrapper': Wrapper})
     >>> result = component.getMultiAdapter((ProxyFactory(f), request), IResult)
-    >>> result.body.__class__ is Wrapper
+    >>> result.__class__ is Wrapper
     True
-    >>> result.body.file is f
+    >>> result.file is f
     True
 
-    >>> result.headers
-    (('Content-Length', '38'),)
+    >>> request.response.getHeader('content-length')
+    '38'
 
+    >>> request = TestRequest(environ={'wsgi.file_wrapper': Wrapper})
     >>> result = component.getMultiAdapter((ProxyFactory(t), request), IResult)
-    >>> result.body.__class__ is Wrapper
+    >>> result.__class__ is Wrapper
     True
-    >>> result.body.file is t
+    >>> result.file is t
     True
 
-    >>> result.headers
-    (('Content-Length', '43'),)
+    >>> request.response.getHeader('content-length')
+    '43'
 
 Normally, the file given to FileResult must be seekable and the entire
 file is used.  The adapters figure out the file size to determine a
@@ -90,9 +92,9 @@
     >>> print f.tell()
     7
 
-    >>> result.headers
-    ()
+    >>> request.response.getHeader('content-length')
+    '10'
 
-Note, that you should really only use file returns for large results.
-Files use file descriptors which can be somewhat scarece resources on
-some systems.  Only use them when you needs them.
+Note that you should really only use file returns for large results.
+Files use file descriptors which can be somewhat scarce resources on
+some systems.  Only use them when you need them.

Modified: Zope3/trunk/src/zope/publisher/http.py
===================================================================
--- Zope3/trunk/src/zope/publisher/http.py	2007-04-17 21:44:47 UTC (rev 74218)
+++ Zope3/trunk/src/zope/publisher/http.py	2007-04-17 21:45:57 UTC (rev 74219)
@@ -34,6 +34,7 @@
 from zope.publisher.interfaces.http import IHTTPApplicationRequest
 from zope.publisher.interfaces.http import IHTTPPublisher
 from zope.publisher.interfaces.http import IHTTPVirtualHostChangedEvent
+from zope.publisher.interfaces.http import IResult
 
 from zope.publisher.interfaces import Redirect
 from zope.publisher.interfaces.http import IHTTPResponse
@@ -597,29 +598,7 @@
         return d.keys()
 
 
-class IResult(interface.Interface):
-    """HTTP result.
 
-    WARNING! This is a PRIVATE interface and VERY LIKELY TO CHANGE!
-
-    The result provides the result in a form suitable for delivery to HTTP
-    clients.
-
-    IMPORTANT: The result object may be held indefinitely by a server and may
-    be accessed by arbitrary threads. For that reason the result should not
-    hold on to any application resources and should be prepared to be invoked
-    from any thread.
-    """
-
-    headers = interface.Attribute(
-        'A sequence of tuples of result headers, such as '
-        '"Content-Type" and "Content-Length", etc.')
-
-    body = interface.Attribute(
-        'An iterable that provides the body data of the response.')
-
-
-
 class HTTPResponse(BaseResponse):
     interface.implements(IHTTPResponse, IHTTPApplicationResponse)
 
@@ -776,35 +755,38 @@
 
 
     def setResult(self, result):
+        'See IHTTPResponse'
         if IResult.providedBy(result):
             r = result
         else:
             r = component.queryMultiAdapter((result, self._request), IResult)
             if r is None:
                 if isinstance(result, basestring):
-                    body, headers = self._implicitResult(result)
-                    r = DirectResult((body,), headers)
+                    r = result
                 elif result is None:
-                    body, headers = self._implicitResult('')
-                    r = DirectResult((body,), headers)
+                    r = ''
                 else:
                     raise TypeError(
-                        'The result should be adaptable to IResult.')
+                        'The result should be None, a string, or adaptable to '
+                        'IResult.')
+            if isinstance(r, basestring):
+                r, headers = self._implicitResult(r)
+                self._headers.update(dict((k, [v]) for (k, v) in headers))
+                r = (r,) # chunking should be much larger than per character
 
         self._result = r
-        self._headers.update(dict([(k, [v]) for (k, v) in r.headers]))
         if not self._status_set:
             self.setStatus(200)
 
 
     def consumeBody(self):
         'See IHTTPResponse'
-        return ''.join(self._result.body)
+        return ''.join(self._result)
 
 
     def consumeBodyIter(self):
         'See IHTTPResponse'
-        return self._result.body
+        return self._result
 
 
     def _implicitResult(self, body):
@@ -1011,22 +993,14 @@
 class DirectResult(object):
     """A generic result object.
 
-    The result's body can be any iteratable. It is the responsibility of the
+    The result's body can be any iterable. It is the responsibility of the
     application to specify all headers related to the content, such as the
     content type and length.
     """
     interface.implements(IResult)
 
-    def __init__(self, body, headers=()):
+    def __init__(self, body):
         self.body = body
-        self.headers = headers
 
-
-def StrResult(body, headers=()):
-    """A simple string result that represents any type of data.
-
-    It is the responsibility of the application to specify all the headers,
-    including content type and length.
-    """
-    return DirectResult((body,), headers)
-
+    def __iter__(self):
+        return iter(self.body)

Modified: Zope3/trunk/src/zope/publisher/httpresults.txt
===================================================================
--- Zope3/trunk/src/zope/publisher/httpresults.txt	2007-04-17 21:44:47 UTC (rev 74218)
+++ Zope3/trunk/src/zope/publisher/httpresults.txt	2007-04-17 21:45:57 UTC (rev 74219)
@@ -2,8 +2,7 @@
 =====================
 
 This document describes the state of creating HTTP results for Zope
-3.2.  This is different than it was in the past and likely to be
-different from how it will be in the future. Please bear with us.
+3.4.  This is different than it was in the past.
 
 Traditionally in Zope, HTTP results are created by simply returning
 strings.  Strings are inspected to deduce their content type, which is
@@ -24,8 +23,9 @@
 disabled the response write method for now.  Maybe we'll reinstate it
 in the future.
 
-There is currently no support for streaming, but there is now support
-for returning large amounts of data.
+There is currently no support for streaming (at least while holding on
+to a database connection and transaction), but there is now support for
+returning large amounts of data.
 
 Returning large amounts of data without storing the data in memory
 ------------------------------------------------------------------
@@ -42,3 +42,97 @@
 It will also take care of positioning the file to it's beginning, 
 so applications don't need to do this beforehand.
 
+This is actually accomplished via zope.app.wsgi.fileresult.FileResult,
+and happens if and only if that, or something like it, is registered as
+an adapter.  The FileResult, however, does what needs to happen thanks
+to a special hook associated with the IResult interface, used by the
+http module in this package.
+
+IResult
+-------
+
+The interface for IResult, found in zope/publisher/interfaces/http.py,
+describes the interface thoroughly.  The IHTTPResponse.setHeader method
+that uses it also documents how it is used.  Reading the IResult
+interface and the IHTTPResponse.setHeader description (in the same
+interface file) is highly recommended.
+
+In addition to supporting sending large amoounts of data, IResult
+supports postprocessing of output.  setResult tries to adapt everything
+to IResult. Postprocessing might include XSLT transforms, adding an
+O-wrap around the content, adding JavaScript and CSS header lines on the
+basis of elements added to a page, or pipelining somehow to do all of it
+sequentially.  May the best approach win! This merely makes the
+different options possible.
+
+To close, we'll build a quick example so you can see it working.
+
+    >>> import zope.interface
+    >>> import zope.component
+    >>> from zope.publisher.browser import TestRequest
+    >>> from zope.publisher.interfaces.http import IResult, IHTTPRequest
+    >>> import cgi
+    >>> @zope.interface.implementer(IResult)
+    ... @zope.component.adapter(unicode, IHTTPRequest)
+    ... def do_something_silly_to_unicode_results(val, request):
+    ...     request.response.setHeader('X-Silly', 'Yes')
+    ...     return (u'<html>\n<head>\n<title>raw</title>\n</head>\n<body>\n' +
+    ...             cgi.escape(val) + '\n</body>\n</html>')
+    ...
+    >>> zope.component.provideAdapter(do_something_silly_to_unicode_results)
+
+That's returning a unicode string, which is special cased to (1) make an
+iterable that is chunked, (2) encode, and (3) set content-length.  
+
+    >>> request = TestRequest()
+    >>> request.response.setHeader('content-type', 'text/html')
+    >>> request.response.setResult(u'<h1>Foo!</h1>')
+    >>> request.response.getHeader('x-silly')
+    'Yes'
+    >>> request.response.getHeader('content-type')
+    'text/html;charset=utf-8'
+    >>> res = tuple(request.response.consumeBodyIter())
+    >>> res
+    ('<html>\n<head>\n<title>raw</title>\n</head>\n<body>\n&lt;h1&gt;Foo!&lt;/h1&gt;\n</body>\n</html>',)
+    >>> len(res[0]) == int(request.response.getHeader('content-length'))
+    True
+
+You can also do everything yourself by returning any non-basestring iterable
+(for instance, a list or tuple).
+
+    >>> @zope.interface.implementer(IResult)
+    ... @zope.component.adapter(int, IHTTPRequest)
+    ... def do_something_silly_to_int_results(val, request):
+    ...     return ['This', ' is an int: %i' % (val,),]
+    ...
+    >>> zope.component.provideAdapter(do_something_silly_to_int_results)
+
+    >>> request = TestRequest()
+    >>> request.response.setHeader('content-type', 'text/plain')
+    >>> request.response.setResult(42)
+    >>> request.response.getHeader('content-type')
+    'text/plain'
+    >>> res = tuple(request.response.consumeBodyIter())
+    >>> res
+    ('This', ' is an int: 42')
+    >>> request.response.getHeader('content-length') is None
+    True
+
+Again, READ THE INTERFACES.  One important bit is that you can't hold on to
+a database connection in one of these iterables.
+
+You can bypass the adaptation by calling `setResult` with an object that
+provides IResult.  The ``DirectResult`` class in the http module is the
+simplest way to do this, but any other IResult should work.
+
+    >>> from zope.publisher.http import DirectResult
+    >>> @zope.interface.implementer(IResult)
+    ... @zope.component.adapter(DirectResult, IHTTPRequest)
+    ... def dont_touch_this(val, request):
+    ...     raise ValueError('boo!  hiss!') # we don't get here.
+    ...
+    >>> request = TestRequest()
+    >>> request.response.setResult(DirectResult(('hi',)))
+    >>> tuple(request.response.consumeBodyIter())
+    ('hi',)
+    

Modified: Zope3/trunk/src/zope/publisher/interfaces/http.py
===================================================================
--- Zope3/trunk/src/zope/publisher/interfaces/http.py	2007-04-17 21:44:47 UTC (rev 74218)
+++ Zope3/trunk/src/zope/publisher/interfaces/http.py	2007-04-17 21:45:57 UTC (rev 74219)
@@ -269,6 +269,41 @@
         """
 
 
+class IResult(Interface):
+    """An iterable that provides the body data of the response.
+    
+    For simplicity, an adapter to this interface may in fact return any
+    iterable, without needing to strictly have the iterable provide
+    IResult.
+
+    IMPORTANT: The result object may be held indefinitely by a server
+    and may be accessed by arbitrary threads. For that reason the result
+    should not hold on to any application resources (i.e., should not
+    have a connection to the database) and should be prepared to be
+    invoked from any thread.
+
+    This iterable should generally be appropriate for WSGI iteration.
+
+    Each element of the iteration should generally be much larger than a
+    character or line; concrete advice on chunk size is hard to come by,
+    but a single chunk of even 100 or 200 K is probably fine.
+
+    If the IResult is a string, then, the default iteration of
+    per-character is wildly too small.  Because this is such a common
+    case, if a string is used as an IResult then this is special-cased
+    to simply convert to a tuple of one value, the string.
+    
+    Adaptation to this interface provides the opportunity for efficient file
+    delivery, pipelining hooks, and more.
+    """
+
+    def __iter__():
+        """iterate over the values that should be returned as the result.
+        
+        See IHTTPResponse.setResult.
+        """
+
+
 class IHTTPResponse(IResponse):
     """An object representation of an HTTP response.
 
@@ -376,8 +411,38 @@
         """
 
     def setResult(result):
-        """Sets the response result value to a string or a file.
-        """
+        """Sets response result value based on input.
+        
+        Input is usually a unicode string, a string, None, or an object
+        that can be adapted to IResult with the request.  The end result
+        is an iterable such as WSGI prefers, determined by following the
+        process described below.
+        
+        Try to adapt the given input, with the request, to IResult
+        (found above in this file).  If this fails, and the original
+        value was a string, use the string as the result; or if was
+        None, use an empty string as the result; and if it was anything
+        else, raise a TypeError.
+        
+        If the result of the above (the adaptation or the default
+        handling of string and None) is unicode, encode it (to the
+        preferred encoding found by adapting the request to
+        zope.i18n.interfaces.IUserPreferredCharsets, usually implemented
+        by looking at the HTTP Accept-Charset header in the request, and
+        defaulting to utf-8) and set the proper encoding information on
+        the Content-Type header, if present.  Otherwise (the end result
+        was not unicode) application is responsible for setting
+        Content-Type header encoding value as necessary.
+        
+        If the result of the above is a string, set the Content-Length
+        header, and make the string be the single member of an iterable
+        such as a tuple (to send large chunks over the wire; see
+        discussion in the IResult interface).  Otherwise (the end result
+        was not a string) application is responsible for setting
+        Content-Length header as necessary.
+        
+        Set the result of all of the above as the response's result. If
+        the status has not been set, set it to 200 (OK). """
 
     def consumeBody():
         """Returns the response body as a string.

Modified: Zope3/trunk/src/zope/publisher/tests/test_http.py
===================================================================
--- Zope3/trunk/src/zope/publisher/tests/test_http.py	2007-04-17 21:44:47 UTC (rev 74218)
+++ Zope3/trunk/src/zope/publisher/tests/test_http.py	2007-04-17 21:45:57 UTC (rev 74219)
@@ -17,12 +17,14 @@
 $Id$
 """
 import unittest
+from zope.testing import doctest
+import zope.testing.cleanup
 
 import zope.event
 from zope.interface import implements
 from zope.publisher.interfaces.logginginfo import ILoggingInfo
 from zope.publisher.http import HTTPRequest, HTTPResponse
-from zope.publisher.http import HTTPInputStream, StrResult
+from zope.publisher.http import HTTPInputStream, DirectResult
 from zope.publisher.publish import publish
 from zope.publisher.base import DefaultPublication
 from zope.publisher.interfaces.http import IHTTPRequest, IHTTPResponse
@@ -539,7 +541,7 @@
 
         # Output the data
         data = 'a'*10
-        response.setResult(StrResult(data))
+        response.setResult(DirectResult(data))
 
         headers, body = self._parseResult(response)
         # Check that the data have been written, and that the header
@@ -648,12 +650,16 @@
         self.failUnless('foo=bar;' in c)
         self.failIf('secure' in c)
 
+def cleanUp(test):
+    zope.testing.cleanup.cleanUp()
 
 def test_suite():
     suite = unittest.TestSuite()
     suite.addTest(unittest.makeSuite(ConcreteHTTPTests))
     suite.addTest(unittest.makeSuite(TestHTTPResponse))
     suite.addTest(unittest.makeSuite(HTTPInputStreamTests))
+    suite.addTest(doctest.DocFileSuite(
+        '../httpresults.txt', setUp=cleanUp, tearDown=cleanUp))
     return suite
 
 

Modified: Zope3/trunk/src/zope/publisher/xmlrpc.py
===================================================================
--- Zope3/trunk/src/zope/publisher/xmlrpc.py	2007-04-17 21:44:47 UTC (rev 74218)
+++ Zope3/trunk/src/zope/publisher/xmlrpc.py	2007-04-17 21:45:57 UTC (rev 74219)
@@ -114,11 +114,10 @@
                 self.handleException(sys.exc_info())
                 return
 
-        super(XMLRPCResponse, self).setResult(
-            DirectResult((body,),
-                         [('content-type', 'text/xml;charset=utf-8'),
-                          ('content-length', str(len(body)))])
-            )
+        headers = [('content-type', 'text/xml;charset=utf-8'),
+                   ('content-length', str(len(body)))]
+        self._headers.update(dict((k, [v]) for (k, v) in headers))
+        super(XMLRPCResponse, self).setResult(DirectResult((body,)))
 
 
     def handleException(self, exc_info):



More information about the Checkins mailing list