[Zope3-checkins] SVN: Zope3/trunk/src/zope/app/wsgi/ I have improved the tests to demonstrate clearly how to use the WSGI

Stephan Richter srichter at cosmos.phy.tufts.edu
Tue Apr 12 11:14:52 EDT 2005


Log message for revision 29949:
  I have improved the tests to demonstrate clearly how to use the WSGI 
  PublisherApp class. I also have created some interfaces for better 
  documentation. Next I will try to create a ZServer-based WSGI server.
  
  

Changed:
  U   Zope3/trunk/src/zope/app/wsgi/README.txt
  U   Zope3/trunk/src/zope/app/wsgi/__init__.py
  A   Zope3/trunk/src/zope/app/wsgi/interfaces.py
  U   Zope3/trunk/src/zope/app/wsgi/tests.py

-=-
Modified: Zope3/trunk/src/zope/app/wsgi/README.txt
===================================================================
--- Zope3/trunk/src/zope/app/wsgi/README.txt	2005-04-12 13:58:33 UTC (rev 29948)
+++ Zope3/trunk/src/zope/app/wsgi/README.txt	2005-04-12 15:14:51 UTC (rev 29949)
@@ -2,42 +2,206 @@
 Zope WSGI Application
 =====================
 
-About
------
-This package contains a WSGI application for Zope.
+This package contains an interpretation of the WSGI specification (PEP-0333)
+for the Zope application server by providing a WSGI application object. First,
+we create a stream that will contain the response:
 
-WSGI is the Python Web Server Gateway Interface, an
-upcoming PEP to standardize the interface between web servers
-and python applications to promote portability.
+  >>> import StringIO
+  >>> data = StringIO.StringIO('')
 
-For more information, refer to the WSGI specification: http://www.python.org/peps/pep-0333.html
+Usually this stream is created by the HTTP server and refers to the output
+stream sent to the server client.
 
-Usage
------
-To use Zope as a WSGI application, the following steps must be taken
+Now that we have our output stream, we need to initialize the WSGI-compliant
+Zope application that is called from the server. To do that, we first have to
+create and open a ZODB connection:
 
-* configure and setup Zope
+  >>> from ZODB.MappingStorage import MappingStorage
+  >>> from ZODB.DB import DB
+  
+  >>> storage = MappingStorage('test.db')
+  >>> db = DB(storage, cache_size=4000)
 
-* create an instance of ``zope.app.wsgi.PublisherApp`` must be created
-  with a refernce to an open database
+We can now initialize the application:
 
-* This instance must be set as the WSGI servers application object
+  >>> from zope.app import wsgi
+  >>> app = wsgi.PublisherApp(db)
 
+The callable ``app`` object accepts two positional arguments, the environment
+and the function that initializes the response and returns a function with
+which the output data can be written.
 
-Example::
+Even though this is commonly done by the server, our first task is to create
+an appropriate environment for the request.
 
+  >>> environ = {
+  ...     'PATH_INFO': '/',
+  ...     'wsgi.input': StringIO.StringIO('')}
+
+Next we create a WSGI-compliant ``start_response()`` method that accepts the
+status of the response to the HTTP request and the headers that are part of
+the response stream. The headers are expected to be a list of
+2-tuples. However, for the purpose of this demonstration we simply ignore all
+the arguments and push a simple message to the stream. The
+``start_response()`` funtion must also return a ``write()`` callable that
+accepts further data.
+
+  >>> def start_response(status, headers):
+  ...     data.write('status and headers.\n\n')
+  ...     return data.write
+  ...
+
+Now we can send the fabricated HTTP request to the application for processing:
+
+  >>> app(environ, start_response)
+  ''
+
+We expect the output of this call to be always an empty string, since all the
+output is written to the output stream directly. Looking at the output 
+
+  >>> print data.getvalue()
+  status and headers.
+  <BLANKLINE>
+  <html><head><title>Unauthorized</title></head>
+  <body><h2>Unauthorized</h2>
+  A server error occurred.
+  </body></html>
+  <BLANKLINE>
+
+we can see that application really crashed and did not know what to do. This
+is okay, since we have not setup anything. Getting a request successfully
+processed would require us to bring up a lot of Zope 3's system, which would
+be just a little bit too much for this demonstration.
+
+Now that we have seen the manual way of initializing and using the publisher
+application, here is the way it is done using all of Zope 3's setup machinery::
+
     from zope.app.server.main import setup, load_options
     from zope.app.wsgi import PublisherApp
 
+    # Read all configuration files and bring up the component architecture
     args = ["-C/path/to/zope.conf"]
     db = setup(load_options(args))
 
-    my_app = PublisherApp(db)
+    # Initialize the WSGI-compliant publisher application with the database
+    wsgiApplication = PublisherApp(db)
 
-    wsgi_server.set_app(my_app)
+    # Here is an example on how the application could be registered with a
+    # WSGI-compliant server. Note that the ``setApplication()`` method is not
+    # part of the PEP 333 specification.
+    wsgiServer.setApplication(wsgiApplication)
 
-This assumes, that Zope is available on the ``PYTHONPATH``.
-Note that you may have to edit ``zope.conf`` to provide
-an absolute path for ``site.zcml``.
+The code above assumes, that Zope is available on the ``PYTHONPATH``.  Note
+that you may have to edit ``zope.conf`` to provide an absolute path for
+``site.zcml``. Unfortunately we do not have enough information about the
+directory structure to make this code a doctest.
 
+In summary, to use Zope as a WSGI application, the following steps must be
+taken:
 
+* configure and setup Zope
+
+* an instance of ``zope.app.wsgi.PublisherApp`` must be created with a
+  refernce to the opened database
+
+* this application instance must be somehow communicated to the WSGI server,
+  i.e. by calling a method on the server that sets the application.
+
+
+The ``IWSGIOutput`` Component
+-----------------------------
+
+Under the hood the WSGI support uses a component that implements
+``IWSGIOutput`` that manages the response headers and provides the output
+stream by implementing the ``write()`` method. In the following text the
+functionality of this class is introduced:  
+
+First, we reset our output stream:
+
+  >>> data.__init__('')
+
+Then we initialize an instance of the WSGI output object:
+
+  >>> output = wsgi.WSGIOutput(start_response)
+
+You can set the response status
+
+  >>> output.setResponseStatus("200", "OK")
+  >>> output._statusString
+  '200 OK'
+
+or set arbitrary headers as a mapping:
+
+  >>> output.setResponseHeaders({'a':'b', 'c':'d'})
+
+The headers must be returned as a list of tuples:
+
+  >>> output.getHeaders()
+  [('a', 'b'), ('c', 'd')]
+  
+Calling ``setResponseHeaders()`` again adds new values:
+
+  >>> output.setResponseHeaders({'x':'y', 'c':'d'})
+  >>> h = output.getHeaders()
+  >>> h.sort()
+  >>> h
+  [('a', 'b'), ('c', 'd'), ('x', 'y')]
+
+Headers that can potentially repeat are added using
+``appendResponseHeaders()``:
+
+  >>> output.appendResponseHeaders(['foo: bar'])
+  >>> h = output.getHeaders()
+  >>> h.sort()
+  >>> h    
+  [('a', 'b'), ('c', 'd'), ('foo', ' bar'), ('x', 'y')]
+  >>> output.appendResponseHeaders(['foo: bar'])
+  >>> h = output.getHeaders()
+  >>> h.sort()
+  >>> h    
+  [('a', 'b'), ('c', 'd'), ('foo', ' bar'), ('foo', ' bar'), ('x', 'y')]
+
+Headers containing a colon should also work
+
+  >>> output.appendResponseHeaders(['my: brain:hurts'])
+  >>> h = output.getHeaders()
+  >>> h.sort()
+  >>> h
+  [('a', 'b'), ('c', 'd'), ('foo', ' bar'), ('foo', ' bar'), 
+   ('my', ' brain:hurts'), ('x', 'y')]
+
+The headers should not be written to the output
+
+  >>> output.wroteResponseHeader()
+  False
+  >>> data.getvalue()
+  ''
+
+Let's now write something to the output stream:
+
+  >>> output.write('Now for something')
+
+The headers should be sent and the data written to the stream:
+
+  >>> output.wroteResponseHeader()
+  True
+  >>> data.getvalue()
+  'status and headers.\n\nNow for something'
+
+Calling write again the headers should not be sent again
+
+  >>> output.write(' completly different!')
+  >>> data.getvalue()
+  'status and headers.\n\nNow for something completly different!'
+
+
+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.
+
+For more information, refer to the WSGI specification:
+http://www.python.org/peps/pep-0333.html
+

Modified: Zope3/trunk/src/zope/app/wsgi/__init__.py
===================================================================
--- Zope3/trunk/src/zope/app/wsgi/__init__.py	2005-04-12 13:58:33 UTC (rev 29948)
+++ Zope3/trunk/src/zope/app/wsgi/__init__.py	2005-04-12 15:14:51 UTC (rev 29949)
@@ -20,89 +20,14 @@
 from zope.publisher.interfaces.http import IHeaderOutput
 
 from zope.app.publication.httpfactory import HTTPPublicationRequestFactory
+from zope.app.wsgi import interfaces
 
-class WsgiOutput(object):
-    """This class handles the output generated by
-    the publisher. It is used to collect the headers
-    and as an outstream to output the response body.
-
-    When write is first called by the response, it initiates
-    the reponse by invoking the WSGI start_response callable.
-
-    Create a mock implementation of the wsgi write callable
-    >>> from StringIO import StringIO
-    >>> data = StringIO('')
-    >>> def start_response(status, headers):
-    ...     data.write('status and headers.')
-    ...     return data.write
-    ...
-
-    create an instance
-    >>> output = WsgiOutput(start_response)
-
-    Set the response status
-    >>> output.setResponseStatus("200", "OK")
-    >>> output._statusString
-    '200 OK'
-    
-    Set the headers as a mapping
-    >>> output.setResponseHeaders({'a':'b', 'c':'d'})
-
-    They must be returned as a list of tuples
-    >>> output.getHeaders()
-    [('a', 'b'), ('c', 'd')]
-    
-    calling setResponseHeaders again adds new values 
-    >>> output.setResponseHeaders({'x':'y', 'c':'d'})
-    >>> h = output.getHeaders()
-    >>> h.sort()
-    >>> h
-    [('a', 'b'), ('c', 'd'), ('x', 'y')]
-
-    Headers that can potentially repeat are added using
-    appendResponseHeaders
-    >>> output.appendResponseHeaders(['foo: bar'])
-    >>> h = output.getHeaders()
-    >>> h.sort()
-    >>> h    
-    [('a', 'b'), ('c', 'd'), ('foo', ' bar'), ('x', 'y')]
-    >>> output.appendResponseHeaders(['foo: bar'])
-    >>> h = output.getHeaders()
-    >>> h.sort()
-    >>> h    
-    [('a', 'b'), ('c', 'd'), ('foo', ' bar'), ('foo', ' bar'), ('x', 'y')]
-
-    Headers containing a colon should also work
-    >>> output.appendResponseHeaders(['my: brain:hurts'])
-    >>> h = output.getHeaders()
-    >>> h.sort()
-    >>> h    
-    [('a', 'b'), ('c', 'd'), ('foo', ' bar'), \
-('foo', ' bar'), ('my', ' brain:hurts'), ('x', 'y')]
-
-    The headers should not be written to the output
-    >>> output.wroteResponseHeader()
-    False
-    >>> data.getvalue()
-    ''
-    
-    now write something
-    >>> output.write('Now for something')
-
-    The headers should be sent and the data written to the stream
-    >>> output.wroteResponseHeader()
-    True
-    >>> data.getvalue()
-    'status and headers.Now for something'
-
-    calling write again the headers should not be sent again
-    >>> output.write(' completly different!')
-    >>> data.getvalue()
-    'status and headers.Now for something completly different!'
+class WSGIOutput(object):
+    """This class handles the output generated by the publisher. It is used to
+    collect the headers and as an outstream to output the response body.
     """
+    implements(interfaces.IWSGIOutput)
 
-    implements(IHeaderOutput)
-
     def __init__(self, start_response):
         self._headers = {}
         self._accumulatedHeaders = []
@@ -112,55 +37,41 @@
         self.wsgi_write = None
         self.start_response = start_response
 
-    def setResponseStatus(self,status, reason):
-        """Sets the status code and the accompanying message.
-        """
-        self._statusString = str(status)+' '+reason
+    def setResponseStatus(self, status, reason):
+        """See zope.publisher.interfaces.http.IHeaderOutput"""
+        self._statusString = str(status) + ' ' + reason
 
     def setResponseHeaders(self, mapping):
-        """Sets headers.  The headers must be Correctly-Cased.
-        """
+        """See zope.publisher.interfaces.http.IHeaderOutput"""
         self._headers.update(mapping)
 
     def appendResponseHeaders(self, lst):
-        """Sets headers that can potentially repeat.
-
-        Takes a list of strings.
-        """
+        """See zope.publisher.interfaces.http.IHeaderOutput"""
         self._accumulatedHeaders.extend(lst)
 
     def wroteResponseHeader(self):
-        """Returns a flag indicating whether the response
-
-        header has already been sent.
-        """
+        """See zope.publisher.interfaces.http.IHeaderOutput"""
         return self._headersSent
 
     def setAuthUserName(self, name):
-        """Sets the name of the authenticated user so the name can be logged.
-        """
+        """See zope.publisher.interfaces.http.IHeaderOutput"""
         pass
 
     def getHeaders(self):
-        """return the response headers as a list of tuples according
-        to the WSGI spec
-        """
+        """See zope.app.wsgi.interfaces.IWSGIOutput"""
         response_headers = self._headers.items()
 
-        accum = [ tuple(line.split(':',1)) for line in self._accumulatedHeaders]
+        accum = [tuple(line.split(':', 1))
+                 for line in self._accumulatedHeaders]
             
         response_headers.extend(accum)
         return response_headers
 
-
     def write(self, data):
-        """write the response.
-        If the reponse has not begun, call the wsgi servers
-        'start_reponse' callable to begin the response
-        """
+        """See zope.app.wsgi.interfaces.IWSGIOutput"""
         if not self._headersSent:
             self.wsgi_write = self.start_response(self._statusString,
-                                         self.getHeaders())
+                                                  self.getHeaders())
             self._headersSent = True
 
         self.wsgi_write(data)
@@ -169,34 +80,33 @@
 class PublisherApp(object):
     """A WSGI application implemenation for the zope publisher
 
-    Instances of this class can be used as a WSGI application
-    object.
+    Instances of this class can be used as a WSGI application object.
 
     The class relies on a properly initialized request factory.
-    
     """
+    implements(interfaces.IWSGIApplication)
     
-
     def __init__(self, db=None, factory=HTTPPublicationRequestFactory):
-        self.request_factory = None
+        self.requestFactory = None
 
         if db is not None:
-            self.request_factory = factory(db)
+            self.requestFactory = factory(db)
 
-    def __call__(self, env, start_response):
-        """makes instances a WSGI callable application object
-        """
-        wsgi_output = WsgiOutput(start_response)
+    def __call__(self, environ, start_response):
+        """See zope.app.wsgi.interfaces.IWSGIApplication"""
+        # wsgiOutput has two purposes: (1) it is the response headers output
+        # manager and (2) it functions as the output stream for the publisher. 
+        wsgiOutput = WSGIOutput(start_response)
 
-        request = self.request_factory( \
-            env['wsgi.input'], wsgi_output, env)
+        request = self.requestFactory(
+            environ['wsgi.input'], wsgiOutput, environ)
 
-        request.response.setHeaderOutput(wsgi_output)
+        request.response.setHeaderOutput(wsgiOutput)
 
         publish(request)
 
         request.close()
 
-        # since the response is written using the WSGI write callable
-        # return an empty iterable (see 
+        # since the response is written using the WSGI ``write()`` callable
+        # return an empty iterable (see PEP 333).
         return ""

Added: Zope3/trunk/src/zope/app/wsgi/interfaces.py
===================================================================
--- Zope3/trunk/src/zope/app/wsgi/interfaces.py	2005-04-12 13:58:33 UTC (rev 29948)
+++ Zope3/trunk/src/zope/app/wsgi/interfaces.py	2005-04-12 15:14:51 UTC (rev 29949)
@@ -0,0 +1,74 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""WSGI-specific and compatible interfaces
+
+See PEP-0333 for details. 
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+
+from zope.interface import Interface
+from zope.publisher.interfaces.http import IHeaderOutput
+
+
+class IWSGIOutput(IHeaderOutput):
+    """This class handles the output generated by the publisher. It is used to
+    collect the headers and as an outstream to output the response body.
+    """
+
+    def getHeaders():
+        """Return the response headers.
+
+        The headers will be returned as a list of tuples according to the WSGI
+        specification.
+        """
+
+    def write(data):
+        """Write the response to the server.
+
+        If the reponse has not begun, call the WSGI server's
+        ``start_response()`` callable to begin the response.
+        """
+
+class IWSGIApplication(Interface):
+    """A WSGI application."""
+
+    def __call__(environ, start_response):
+        """Called by a WSGI server.
+
+        The ``environ`` parameter is a dictionary object, containing CGI-style
+        environment variables. This object must be a builtin Python dictionary
+        (not a subclass, UserDict or other dictionary emulation), and the
+        application is allowed to modify the dictionary in any way it
+        desires. The dictionary must also include certain WSGI-required
+        variables (described in a later section), and may also include
+        server-specific extension variables, named according to a convention
+        that will be described below.
+
+        The ``start_response`` parameter is a callable accepting two required
+        positional arguments, and one optional argument. For the sake of
+        illustration, we have named these arguments ``status``,
+        ``response_headers``, and ``exc_info``, but they are not required to
+        have these names, and the application must invoke the
+        ``start_response`` callable using positional arguments
+        (e.g. ``start_response(status, response_headers)``).
+        """
+
+
+class IWSGIServer(Interface):
+    """A WSGI server."""
+
+    def set_application(app):
+        """Tells the server about the application to use."""


Property changes on: Zope3/trunk/src/zope/app/wsgi/interfaces.py
___________________________________________________________________
Name: svn:eol-style
   + native

Modified: Zope3/trunk/src/zope/app/wsgi/tests.py
===================================================================
--- Zope3/trunk/src/zope/app/wsgi/tests.py	2005-04-12 13:58:33 UTC (rev 29948)
+++ Zope3/trunk/src/zope/app/wsgi/tests.py	2005-04-12 15:14:51 UTC (rev 29949)
@@ -16,11 +16,21 @@
 $Id$
 """
 import unittest
-from zope.testing.doctestunit import DocTestSuite
+from zope.testing import doctest
+from zope.app.testing import setup, ztapi, placelesssetup
 
+def setUp(test):
+    placelesssetup.setUp(test)
+
+def tearDown(test):
+    placelesssetup.tearDown(test)
+
+
 def test_suite():
     return unittest.TestSuite((
-        DocTestSuite('zope.app.wsgi'),
+        doctest.DocFileSuite('README.txt',
+                             setUp=setUp, tearDown=tearDown,
+                             optionflags=doctest.NORMALIZE_WHITESPACE),
         ))
 
 if __name__ == '__main__':



More information about the Zope3-Checkins mailing list