[Zope-Checkins] SVN: Zope/branches/tseaver-fix_wsgi/src/ZPublisher/WSGIPublisher.py Refactor WSGIHTTPResponse to avoid the need to use str() and parse.

Tres Seaver tseaver at palladion.com
Sat May 29 00:24:47 EDT 2010


Log message for revision 112832:
  Refactor WSGIHTTPResponse to avoid the need to use str() and parse.
  
  o Instead, compute status and headers directly.
  
  Chop out error and transaction handling from the 'publish*' functions:
  the point of doing WSGI is to move that stuff out of the application,
  and out into middleware.
  
  One backward incompatibility:  the special "shutdown" behavior is gone
  here.  It should be replaced by something in view code.
  

Changed:
  U   Zope/branches/tseaver-fix_wsgi/src/ZPublisher/WSGIPublisher.py

-=-
Modified: Zope/branches/tseaver-fix_wsgi/src/ZPublisher/WSGIPublisher.py
===================================================================
--- Zope/branches/tseaver-fix_wsgi/src/ZPublisher/WSGIPublisher.py	2010-05-29 04:24:44 UTC (rev 112831)
+++ Zope/branches/tseaver-fix_wsgi/src/ZPublisher/WSGIPublisher.py	2010-05-29 04:24:46 UTC (rev 112832)
@@ -13,15 +13,12 @@
 """ Python Object Publisher -- Publish Python objects on web servers
 """
 from cStringIO import StringIO
-import sys
 import time
 
-from zExceptions import Redirect
 from ZServer.medusa.http_date import build_http_date
 from ZPublisher.HTTPResponse import HTTPResponse
 from ZPublisher.HTTPRequest import HTTPRequest
 from ZPublisher.mapply import mapply
-from ZPublisher.Publish import Retry
 from ZPublisher.Publish import call_object
 from ZPublisher.Publish import dont_publish_class
 from ZPublisher.Publish import get_module_info
@@ -37,8 +34,8 @@
     """A response object for WSGI
 
     This Response object knows nothing about ZServer, but tries to be
-    compatible with the ZServerHTTPResponse. 
-    
+    compatible with the ZServerHTTPResponse.
+
     Most significantly, streaming is not (yet) supported.
     """
     _streaming = _chunking = 0
@@ -49,22 +46,16 @@
     # Set this value to 1 if streaming output in
     # HTTP/1.1 should use chunked encoding
     http_chunk = 0
-    
-    def __str__(self):
 
-        if self._wrote:
-            if self._chunking:
-                return '0\r\n\r\n'
-            else:
-                return ''
+    def finalize(self):
 
         headers = self.headers
         body = self.body
 
         # set 204 (no content) status if 200 and response is empty
         # and not streaming
-        if ('content-type' not in headers and 
-            'content-length' not in headers and 
+        if ('content-type' not in headers and
+            'content-length' not in headers and
             not self._streaming and self.status == 200):
             self.setStatus('nocontent')
 
@@ -74,21 +65,8 @@
         if content_length is None and not self._streaming:
             self.setHeader('content-length', len(body))
 
-        chunks = []
-        append = chunks.append
-
-        # status header must come first.
-        version = self._http_version or '1.0'
-        append("HTTP/%s %d %s" % (version, self.status, self.errmsg))
-
-        # add zserver headers
-        if self._server_version is not None:
-            append('Server: %s' % self._server_version)
-
-        append('Date: %s' % build_http_date(_now()))
-
         if self._http_version == '1.0':
-            if (self._http_connection == 'keep-alive' and 
+            if (self._http_connection == 'keep-alive' and
                 'content-length' in self.headers):
                 self.setHeader('Connection', 'Keep-Alive')
             else:
@@ -106,137 +84,98 @@
                 else:
                     self.setHeader('Connection','close')
 
-        for key, val in headers.items():
-            if key.lower() == key:
-                # only change non-literal header names
-                key = '-'.join([x.capitalize() for x in key.split('-')])
-            append("%s: %s" % (key, val))
+        return '%s %s' % (self.status, self.errmsg), self.listHeaders()
 
-        if self.cookies:
-            chunks.extend(self._cookie_list())
+    def listHeaders(self):
+        result = []
+        if self._server_version:
+            result.append(('Server', self._server_version))
 
-        for key, value in self.accumulated_headers:
-            append("%s: %s" % (key, value))
+        result.append(('Date', build_http_date(_now())))
+        result.extend(HTTPResponse.listHeaders(self))
+        return result
 
-        append('') # RFC 2616 mandates empty line between headers and payload
-        append(body)
+    def __str__(self):
 
-        return "\r\n".join(chunks)
+        if self._wrote:
+            if self._chunking:
+                return '0\r\n\r\n'
+            else:
+                return ''
 
-def publish(request, module_name, after_list, debug=0,
-            # Optimize:
-            call_object=call_object,
-            missing_name=missing_name,
-            dont_publish_class=dont_publish_class,
-            mapply=mapply,
-            ):
+        chunks = []
+        status, headers = self.finalize()
 
-    (bobo_before, bobo_after, object, realm, debug_mode, err_hook,
-     validated_hook, transactions_manager)= get_module_info(module_name)
+        # status header must come first.
+        version = self._http_version or '1.0'
+        chunks.append("HTTP/%s %s" % (version, status))
 
-    parents=None
-    response=None
-    try:
-        request.processInputs()
+        for key, val in headers:
+            chunks.append("%s: %s" % (key, val))
 
-        request_get=request.get
-        response=request.response
+        # RFC 2616 mandates empty line between headers and payload
+        chunks.append('')
+        chunks.append(self.body)
 
-        # First check for "cancel" redirect:
-        if request_get('SUBMIT','').strip().lower()=='cancel':
-            cancel=request_get('CANCEL_ACTION','')
-            if cancel:
-                raise Redirect, cancel
+        return "\r\n".join(chunks)
 
-        after_list[0]=bobo_after
-        if debug_mode:
-            response.debug_mode=debug_mode
-        if realm and not request.get('REMOTE_USER',None):
-            response.realm=realm
+def publish(request, module_name, after_list, debug=0):
 
-        if bobo_before is not None:
-            bobo_before()
+    (bobo_before,
+     bobo_after,
+     object,
+     realm,
+     debug_mode,
+     err_hook,
+     validated_hook,
+     transactions_manager,
+    )= get_module_info(module_name)
 
-        # Get the path list.
-        # According to RFC1738 a trailing space in the path is valid.
-        path=request_get('PATH_INFO')
+    request.processInputs()
+    response = request.response
 
-        request['PARENTS']=parents=[object]
+    after_list[0] = bobo_after
 
-        if transactions_manager:
-            transactions_manager.begin()
+    if debug_mode:
+        response.debug_mode = debug_mode
 
-        object=request.traverse(path, validated_hook=validated_hook)
+    if realm and not request.get('REMOTE_USER', None):
+        response.realm = realm
 
-        if transactions_manager:
-            transactions_manager.recordMetaData(object, request)
+    if bobo_before is not None:
+        bobo_before()
 
-        result=mapply(object, request.args, request,
-                      call_object,1,
-                      missing_name,
-                      dont_publish_class,
-                      request, bind=1)
+    # Get the path list.
+    # According to RFC1738 a trailing space in the path is valid.
+    path = request.get('PATH_INFO')
 
-        if result is not response:
-            response.setBody(result)
+    request['PARENTS'] = parents = [object]
 
-        if transactions_manager:
-            transactions_manager.commit()
+    object = request.traverse(path, validated_hook=validated_hook)
 
-        return response
-    except:
+    if transactions_manager:
+        transactions_manager.recordMetaData(object, request)
 
-        # DM: provide nicer error message for FTP
-        sm = None
-        if response is not None:
-            sm = getattr(response, "setMessage", None)
+    result = mapply(object,
+                    request.args,
+                    request,
+                    call_object,
+                    1,
+                    missing_name,
+                    dont_publish_class,
+                    request,
+                    bind=1,
+                    )
 
-        if sm is not None:
-            from asyncore import compact_traceback
-            cl,val= sys.exc_info()[:2]
-            sm('%s: %s %s' % (
-                getattr(cl,'__name__',cl), val,
-                debug_mode and compact_traceback()[-1] or ''))
+    if result is not response:
+        response.setBody(result)
 
-        if err_hook is not None:
-            if parents:
-                parents=parents[0]
-            try:
-                try:
-                    return err_hook(parents, request,
-                                    sys.exc_info()[0],
-                                    sys.exc_info()[1],
-                                    sys.exc_info()[2],
-                                    )
-                except Retry:
-                    if not request.supports_retry():
-                        return err_hook(parents, request,
-                                        sys.exc_info()[0],
-                                        sys.exc_info()[1],
-                                        sys.exc_info()[2],
-                                        )
-            finally:
-                if transactions_manager:
-                    transactions_manager.abort()
+    return response
 
-            # Only reachable if Retry is raised and request supports retry.
-            newrequest=request.retry()
-            request.close()  # Free resources held by the request.
-            try:
-                return publish(newrequest, module_name, after_list, debug)
-            finally:
-                newrequest.close()
-
-        else:
-            if transactions_manager:
-                transactions_manager.abort()
-            raise
-
 def publish_module(environ, start_response):
 
-    must_die=0
-    status=200
-    after_list=[None]
+    status = 200
+    after_list = [None]
     stdout = StringIO()
     stderr = StringIO()
     response = WSGIResponse(stdout=stdout, stderr=stderr)
@@ -248,57 +187,28 @@
 
     # Let's support post-mortem debugging
     handle_errors = environ.get('wsgi.handleErrors', True)
-    
-    try:
-        response = publish(request, 'Zope2', after_list=[None], 
-                           debug=handle_errors)
-    except SystemExit, v:
-        must_die=sys.exc_info()
-        request.response.exception(must_die)
-    except ImportError, v:
-        if isinstance(v, tuple) and len(v)==3: must_die=v
-        elif hasattr(sys, 'exc_info'): must_die=sys.exc_info()
-        else: must_die = SystemExit, v, sys.exc_info()[2]
-        request.response.exception(1, v)
-    except:
-        request.response.exception()
-        status=response.getStatus()
 
+    response = publish(request, 'Zope2', after_list=[None],
+                       debug=handle_errors)
     if response:
         # Start the WSGI server response
-        status = response.getHeader('status')
+        status, headers = response.finalize()
         # ZServerHTTPResponse calculates all headers and things when you
         # call it's __str__, so we need to get it, and then munge out
         # the headers from it. It's a bit backwards, and we might optimize
-        # this by not using ZServerHTTPResponse at all, and making the 
+        # this by not using ZServerHTTPResponse at all, and making the
         # HTTPResponses more WSGI friendly. But this works.
-        result = str(response)
-        headers, body = result.split('\r\n\r\n',1)
-        headers = [tuple(n.split(': ',1)) for n in headers.split('\r\n')[1:]]
         start_response(status, headers)
         # If somebody used response.write, that data will be in the
         # stdout StringIO, so we put that before the body.
         # XXX This still needs verification that it really works.
-        result=(stdout.getvalue(), body)
+        result=(stdout.getvalue(), response.body)
     request.close()
     stdout.close()
 
-    if after_list[0] is not None: after_list[0]()
-    
-    if must_die:
-        # Try to turn exception value into an exit code.
-        try:
-            if hasattr(must_die[1], 'code'):
-                code = must_die[1].code
-            else: code = int(must_die[1])
-        except:
-            code = must_die[1] and 1 or 0
-        if hasattr(request.response, '_requestShutdown'):
-            request.response._requestShutdown(code)
+    if after_list[0] is not None:
+        after_list[0]()
 
-        try: raise must_die[0], must_die[1], must_die[2]
-        finally: must_die=None
-        
     # Return the result body iterable.
     return result
 



More information about the Zope-Checkins mailing list