Robert Brewer avatar Robert Brewer committed 15f965a

Transformed _cptree.wsgi_handler function into _cpwsgi.CPWSGIApp class (a delegate for the Application object), and merged in the wsgi.pipeline functionality. This removes the need to call wsgi.pipeline(app) before tree.mount.

Comments (0)

Files changed (3)

cherrypy/_cptree.py

 """CherryPy Application and Tree objects."""
 
 import os
-import sys
 import cherrypy
-from cherrypy import _cpconfig, _cplogging, tools
-from cherrypy._cperror import format_exc, bare_error
-from cherrypy.lib import http
+from cherrypy import _cpconfig, _cplogging, _cpwsgi, tools
 
 
 class Application(object):
         self.log = _cplogging.LogManager(id(self))
         self.root = root
         self.script_name = script_name
+        self.wsgiapp = _cpwsgi.CPWSGIApp(self)
         self.namespaces = {"log": lambda k, v: setattr(self.log, k, v),
+                           "wsgi": self.wsgiapp.namespace_handler,
                            }
         self.config = {}
     
         # Handle namespaces specified in config.
         _cpconfig._call_namespaces(self.config.get("/", {}), self.namespaces)
     
-    def wsgiapp(self, environ, start_response):
-        # This is here instead of __call__ because it's really hard
-        # to overwrite special methods like __call__ per instance.
-        return wsgi_handler(environ, start_response, app=self)
-    
     def __call__(self, environ, start_response):
         return self.wsgiapp(environ, start_response)
 
 
-headerNames = {'HTTP_CGI_AUTHORIZATION': 'Authorization',
-               'CONTENT_LENGTH': 'Content-Length',
-               'CONTENT_TYPE': 'Content-Type',
-               'REMOTE_HOST': 'Remote-Host',
-               'REMOTE_ADDR': 'Remote-Addr',
-               }
-
-def translate_headers(environ):
-    """Translate CGI-environ header names to HTTP header names."""
-    for cgiName in environ:
-        # We assume all incoming header keys are uppercase already.
-        if cgiName in headerNames:
-            yield headerNames[cgiName], environ[cgiName]
-        elif cgiName[:5] == "HTTP_":
-            # Hackish attempt at recovering original header names.
-            translatedHeader = cgiName[5:].replace("_", "-")
-            yield translatedHeader, environ[cgiName]
-
-
-def wsgi_handler(environ, start_response, app):
-    request = None
-    try:
-        env = environ.get
-        local = http.Host('', int(env('SERVER_PORT', 80)),
-                          env('SERVER_NAME', ''))
-        remote = http.Host(env('REMOTE_ADDR', ''),
-                           int(env('REMOTE_PORT', -1)),
-                           env('REMOTE_HOST', ''))
-        request = cherrypy.engine.request(local, remote,
-                                          env('wsgi.url_scheme'),
-                                          env('ACTUAL_SERVER_PROTOCOL', "HTTP/1.1"))
-        
-        # LOGON_USER is served by IIS, and is the name of the
-        # user after having been mapped to a local account.
-        # Both IIS and Apache set REMOTE_USER, when possible.
-        request.login = env('LOGON_USER') or env('REMOTE_USER') or None
-        
-        request.multithread = environ['wsgi.multithread']
-        request.multiprocess = environ['wsgi.multiprocess']
-        request.wsgi_environ = environ
-        
-        request.app = app
-        
-        path = env('SCRIPT_NAME', '') + env('PATH_INFO', '')
-        response = request.run(environ['REQUEST_METHOD'], path,
-                               env('QUERY_STRING', ''),
-                               env('SERVER_PROTOCOL'),
-                               translate_headers(environ),
-                               environ['wsgi.input'])
-        s, h, b = response.status, response.header_list, response.body
-        exc = None
-    except (KeyboardInterrupt, SystemExit), ex:
-        try:
-            if request:
-                request.close()
-        except:
-            cherrypy.log(traceback=True)
-        request = None
-        raise ex
-    except:
-        if request and request.throw_errors:
-            raise
-        tb = format_exc()
-        cherrypy.log(tb)
-        if request and not request.show_tracebacks:
-            tb = ""
-        s, h, b = bare_error(tb)
-        exc = sys.exc_info()
-    
-    try:
-        start_response(s, h, exc)
-        for chunk in b:
-            # WSGI requires all data to be of type "str". This coercion should
-            # not take any time at all if chunk is already of type "str".
-            # If it's unicode, it could be a big performance hit (x ~500).
-            if not isinstance(chunk, str):
-                chunk = chunk.encode("ISO-8859-1")
-            yield chunk
-        if request:
-            request.close()
-        request = None
-    except (KeyboardInterrupt, SystemExit), ex:
-        try:
-            if request:
-                request.close()
-        except:
-            cherrypy.log(traceback=True)
-        request = None
-        raise ex
-    except:
-        cherrypy.log(traceback=True)
-        try:
-            if request:
-                request.close()
-        except:
-            cherrypy.log(traceback=True)
-        request = None
-        s, h, b = bare_error()
-        # CherryPy test suite expects bare_error body to be output,
-        # so don't call start_response (which, according to PEP 333,
-        # may raise its own error at that point).
-        for chunk in b:
-            if not isinstance(chunk, str):
-                chunk = chunk.encode("ISO-8859-1")
-            yield chunk
-
-
 class Tree(object):
     """A registry of CherryPy applications, mounted at diverse points.
     

cherrypy/_cpwsgi.py

 """WSGI interface (see PEP 333)."""
 
+import sys as _sys
+
 import cherrypy as _cherrypy
-from cherrypy import _cpwsgiserver
+from cherrypy import _cperror, _cpwsgiserver
 from cherrypy.lib import http as _http
 
 
-class pipeline(list):
-    """An ordered list of configurable WSGI middleware.
+class CPWSGIApp(object):
+    """A WSGI application object for a CherryPy Application.
     
-    self: a list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a
-        constructor that takes an initial, positional wsgiapp argument,
+    pipeline: a list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a
+        constructor that takes an initial, positional 'nextapp' argument,
         plus optional keyword arguments, and returns a WSGI application
         (that takes environ and start_response arguments). The 'name' can
         be any you choose, and will correspond to keys in self.config.
+    
+    head: rather than nest all apps in the pipeline on each call, it's only
+        done the first time, and the result is memoized into self.head. Set
+        this to None again if you change self.pipeline after calling self.
+    
     config: a dict whose keys match names listed in the pipeline. Each
         value is a further dict which will be passed to the corresponding
         named WSGI callable (from the pipeline) as keyword arguments.
     """
     
-    def __new__(cls, app, members=None, key="wsgi"):
-        return list.__new__(cls)
+    pipeline = []
+    head = None
+    config = {}
     
-    def __init__(self, app, members=None, key="wsgi"):
-        self.app = app
-        if members:
-            self.extend(members)
-        self.head = None
-        self.tail = None
-        self.config = {}
-        self.key = key
-        app.namespaces[key] = self.namespace_handler
-        app.wsgi_pipeline = self
+    throws = (KeyboardInterrupt, SystemExit)
+    
+    headerNames = {'HTTP_CGI_AUTHORIZATION': 'Authorization',
+                   'CONTENT_LENGTH': 'Content-Length',
+                   'CONTENT_TYPE': 'Content-Type',
+                   'REMOTE_HOST': 'Remote-Host',
+                   'REMOTE_ADDR': 'Remote-Addr',
+                   }
+    
+    def translate_headers(self, environ):
+        """Translate CGI-environ header names to HTTP header names."""
+        for cgiName in environ:
+            # We assume all incoming header keys are uppercase already.
+            if cgiName in self.headerNames:
+                yield self.headerNames[cgiName], environ[cgiName]
+            elif cgiName[:5] == "HTTP_":
+                # Hackish attempt at recovering original header names.
+                translatedHeader = cgiName[5:].replace("_", "-")
+                yield translatedHeader, environ[cgiName]
+    
+    def __init__(self, cpapp, pipeline=None):
+        self.cpapp = cpapp
+        self.pipeline = self.pipeline[:]
+        if pipeline:
+            self.pipeline.extend(pipeline)
+        self.config = self.config.copy()
+    
+    def get_request(self, environ):
+        env = environ.get
+        
+        local = _http.Host('', int(env('SERVER_PORT', 80)),
+                           env('SERVER_NAME', ''))
+        remote = _http.Host(env('REMOTE_ADDR', ''),
+                            int(env('REMOTE_PORT', -1)),
+                            env('REMOTE_HOST', ''))
+        scheme = env('wsgi.url_scheme')
+        sproto = env('ACTUAL_SERVER_PROTOCOL', "HTTP/1.1")
+        request = _cherrypy.engine.request(local, remote, scheme, sproto)
+        
+        # LOGON_USER is served by IIS, and is the name of the
+        # user after having been mapped to a local account.
+        # Both IIS and Apache set REMOTE_USER, when possible.
+        request.login = env('LOGON_USER') or env('REMOTE_USER') or None
+        request.multithread = environ['wsgi.multithread']
+        request.multiprocess = environ['wsgi.multiprocess']
+        request.wsgi_environ = environ
+        request.app = self.cpapp
+        return request
+    
+    def tail(self, environ, start_response):
+        """WSGI application callable for the actual CherryPy application.
+        
+        You probably shouldn't call this; call self.__call__ instead,
+        so that any WSGI middleware in self.pipeline can run first.
+        """
+        request = None
+        try:
+            request = self.get_request(environ)
+            
+            meth = environ['REQUEST_METHOD']
+            path = environ.get('SCRIPT_NAME', '') + environ.get('PATH_INFO', '')
+            qs = environ.get('QUERY_STRING', '')
+            rproto = environ.get('SERVER_PROTOCOL')
+            headers = self.translate_headers(environ)
+            rfile = environ['wsgi.input']
+            
+            response = request.run(meth, path, qs, rproto, headers, rfile)
+            s, h, b = response.status, response.header_list, response.body
+            exc = None
+        except self.throws, ex:
+            self._close_req(request)
+            raise ex
+        except:
+            if request and request.throw_errors:
+                raise
+            
+            tb = _cperror.format_exc()
+            _cherrypy.log(tb)
+            if request and not request.show_tracebacks:
+                tb = ""
+            s, h, b = _cperror.bare_error(tb)
+            
+            exc = _sys.exc_info()
+        
+        try:
+            start_response(s, h, exc)
+            for chunk in b:
+                # WSGI requires all data to be of type "str". This coercion should
+                # not take any time at all if chunk is already of type "str".
+                # If it's unicode, it could be a big performance hit (x ~500).
+                if not isinstance(chunk, str):
+                    chunk = chunk.encode("ISO-8859-1")
+                yield chunk
+            self._close_req(request)
+        except self.throws, ex:
+            self._close_req(request)
+            raise ex
+        except:
+            _cherrypy.log(traceback=True)
+            self._close_req(request)
+            
+            # CherryPy test suite expects bare_error body to be output,
+            # so don't call start_response (which, according to PEP 333,
+            # may raise its own error at that point).
+            s, h, b = _cperror.bare_error()
+            for chunk in b:
+                if not isinstance(chunk, str):
+                    chunk = chunk.encode("ISO-8859-1")
+                yield chunk
+    
+    def _close_req(self, request):
+        if hasattr(request, "close"):
+            try:
+                request.close()
+            except:
+                _cherrypy.log(traceback=True)
+    
+    def __call__(self, environ, start_response):
+        head = self.head
+        if head is None:
+            # Create and nest the WSGI apps in our pipeline (in reverse order).
+            head = self.tail
+            for name, callable in self.pipeline[::-1]:
+                conf = self.config.get(name, {})
+                head = callable(head, **conf)
+            self.head = head
+        return head(environ, start_response)
     
     def namespace_handler(self, k, v):
-        """Config handler for our namespace."""
+        """Config handler for the 'wsgi' namespace."""
         if k == "pipeline":
-            # Note this allows multiple entries to be aggregated (but also
-            # note dicts are essentially unordered). It should also allow
-            # developers to set default middleware in code (passed to
-            # pipeline.__init__) that deployers can add to but not remove.
-            self.extend(v)
-            
-            if self:
-                # If self is empty, there's no need to replace app.wsgiapp.
-                # Also note we're grabbing app.wsgiapp, not app.__call__,
-                # so we can "play nice" with other Application-manglers
-                # (hopefully, they'll do the same).
-                self.tail = self.app.wsgiapp
-                self.app.wsgiapp = self.__call__
+            # Note this allows multiple 'wsgi.pipeline' config entries
+            # (but each entry will be processed in a 'random' order).
+            # It should also allow developers to set default middleware
+            # in code (passed to self.__init__) that deployers can add to
+            # (but not remove) via config.
+            self.pipeline.extend(v)
         else:
             name, arg = k.split(".", 1)
             bucket = self.config.setdefault(name, {})
             bucket[arg] = v
-    
-    def __call__(self, environ, start_response):
-        if not self.head:
-            # This class may be used without calling namespace_handler,
-            # in which case self.tail may still be None.
-            self.head = self.tail or self.app.wsgiapp
-            pipe = self[:]
-            pipe.reverse()
-            for name, callable in pipe:
-                conf = self.config.get(name, {})
-                self.head = callable(self.head, **conf)
-        return self.head(environ, start_response)
-    
-    def __repr__(self):
-        return "%s.%s(%r)" % (self.__module__, self.__class__.__name__,
-                              list(self))
 
 
 

cherrypy/test/test_wsgi_ns.py

     cherrypy.config.update({'environment': 'test_suite'})
     
     app = cherrypy.Application(Root())
-    p = cherrypy.wsgi.pipeline(app, [('changecase', ChangeCase)])
-    p.config['changecase'] = {'to': 'upper'}
+    app.wsgiapp.pipeline.append(('changecase', ChangeCase))
+    app.wsgiapp.config['changecase'] = {'to': 'upper'}
     cherrypy.tree.mount(app, config={'/': root_conf})
-    
-    # If we do not supply any middleware in code, pipeline is much cleaner:
-    # app = cherrypy.Application(Root())
-    # cherrypy.wsgi.pipeline(app)
-    # cherrypy.tree.mount(app, config={'/': root_conf})
 
 
 from cherrypy.test import helper
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.