Anonymous avatar Anonymous committed e7330b8

Created a branch for some WSGI related ideas that I have implemented.

Comments (0)

Files changed (9)

 from _cperror import WrongConfigValue, TimeoutError
 import config
 
+from _cpwsgi import Application
+
 import _cptools
 tools = _cptools.default_toolbox
 
                     raise cherrypy.NotFound()
                 self.app = cherrypy.tree.apps[r]
             else:
-                self.script_name = self.app.script_name
+                self.script_name = self.wsgi_environ.get('SCRIPT_NAME', '')
             
             # path_info should be the path from the
             # app root (script_name) to the handler.
-            self.path_info = self.path[len(self.script_name.rstrip("/")):]
+            self.path_info = self.wsgi_environ.get('PATH_INFO', '')
             
             # Loop to allow for InternalRedirect.
             pi = self.path_info
-import logging
-import sys
-
-from cherrypy import config
-
-
-class Application:
-    """A CherryPy Application."""
-    
-    def __init__(self, root, script_name="", conf=None):
-        self.access_log = log = logging.getLogger("cherrypy.access.%s" % id(self))
-        log.setLevel(logging.INFO)
-        
-        self.error_log = log = logging.getLogger("cherrypy.error.%s" % id(self))
-        log.setLevel(logging.DEBUG)
-        
-        self.root = root
-        self.script_name = script_name
-        self.conf = {}
-        if conf:
-            self.merge(conf)
-    
-    def merge(self, conf):
-        """Merge the given config into self.config."""
-        config.merge(self.conf, conf)
-        
-        # Create log handlers as specified in config.
-        rootconf = self.conf.get("/", {})
-        config._configure_builtin_logging(rootconf, self.access_log, "log_access_file")
-        config._configure_builtin_logging(rootconf, self.error_log)
-    
-    def guess_abs_path(self):
-        """Guess the absolute URL from server.socket_host and script_name.
-        
-        When inside a request, the abs_path can be formed via:
-            cherrypy.request.base + (cherrypy.request.app.script_name or "/")
-        
-        However, outside of the request we must guess, hoping the deployer
-        set socket_host and socket_port correctly.
-        """
-        port = int(config.get('server.socket_port', 80))
-        if port in (443, 8443):
-            scheme = "https://"
-        else:
-            scheme = "http://"
-        host = config.get('server.socket_host', '')
-        if port != 80:
-            host += ":%s" % port
-        return scheme + host + self.script_name
+from cherrypy._cperror import format_exc, bare_error
+from cherrypy._cpwsgi import Application, HostedWSGI
+from cherrypy import NotFound
+from cherrypy.lib import http
 
 
 class Tree:
-    """A registry of CherryPy applications, mounted at diverse points."""
+    """A dispatcher of WSGI applications, mounted at diverse points."""
     
     def __init__(self):
         self.apps = {}
     
-    def mount(self, root, script_name="", conf=None):
-        """Mount a new app from a root object, script_name, and conf."""
+    def mount(self, app, script_name="", conf=None, wrap=True):
+        """Mount a new app at script_name using configuration in conf.
+        
+        An application can be one of:
+            1) A standard cherrypy.Application - left as is.
+            2) A "root" object - wrapped in an Application instance.
+            3) A  WSGI callable - optionally wrapped in a HostedWSGI instance.
+        
+        If wrap == True, a WSGI callable will be wrapped in a cherrypy.Application
+        instance, allowing the use of tools with the WSGI application.
+        """
+        
         # Next line both 1) strips trailing slash and 2) maps "/" -> "".
         script_name = script_name.rstrip("/")
-        app = Application(root, script_name, conf)
+        
+        # Leave Application objects alone
+        if isinstance(app, Application):
+            pass
+        # Handle "root" objects...
+        elif not callable(app):
+            app = Application(app, script_name, conf)
+        # Handle WSGI callables
+        elif callable(app) and wrap:
+            app = HostedWSGI(app)
+        # In all other cases leave the app intact (no wrapping)
+        
         self.apps[script_name] = app
         
         # If mounted at "", add favicon.ico
-        if script_name == "" and root and not hasattr(root, "favicon_ico"):
+        if script_name == "" and app and not hasattr(app, "favicon_ico"):
             import os
             from cherrypy import tools
             favicon = os.path.join(os.getcwd(), os.path.dirname(__file__),
                                    "favicon.ico")
-            root.favicon_ico = tools.staticfile.handler(favicon)
+            app.favicon_ico = tools.staticfile.handler(favicon)
         
         return app
     
         from cherrypy.lib import http
         return http.urljoin(script_name, path)
 
+    def dispatch(self, environ, start_response):
+        """Dispatch to mounted WSGI applications."""
+        script_name = environ.get("SCRIPT_NAME", '').rstrip('/')
+        path_info = environ.get("PATH_INFO", '')
+        
+        mount_points = self.apps.keys()
+        mount_points.sort()
+        mount_points.reverse()
+        
+        for mp in mount_points:
+            if path_info.startswith(mp):
+                environ['SCRIPT_NAME'] = script_name + mp
+                environ['PATH_INFO'] = path_info[len(mp):]
+                app = self.apps[mp]
+                return app(environ, start_response)
+        raise NotFound
 """A WSGI application interface (see PEP 333)."""
+import logging
+import sys
 
-import sys
 import cherrypy
-from cherrypy import _cpwsgiserver
+from cherrypy import _cpwsgiserver, config
 from cherrypy._cperror import format_exc, bare_error
 from cherrypy.lib import http
 
             translatedHeader = cgiName[5:].replace("_", "-")
             yield translatedHeader, environ[cgiName]
 
+def _init_request(environ):
+    """Initialize and return the cherrypy.request object."""
+    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'))
+    return request
 
-def _wsgi_callable(environ, start_response, app=None):
-    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'))
+class Application:
+    """A CherryPy WSGI Application."""
+    
+    def __init__(self, root, script_name="", conf=None):
+        self.access_log = log = logging.getLogger("cherrypy.access.%s" % id(self))
+        log.setLevel(logging.INFO)
         
-        # 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
+        self.error_log = log = logging.getLogger("cherrypy.error.%s" % id(self))
+        log.setLevel(logging.DEBUG)
         
-        request.multithread = environ['wsgi.multithread']
-        request.multiprocess = environ['wsgi.multiprocess']
-        request.wsgi_environ = environ
+        self.root = root
+        self.script_name = script_name
+        self.conf = {}
+        if conf:
+            self.merge(conf)
+
+    def __call__(self, environ, start_response):
+        if not getattr(cherrypy.request, 'initialized', False):
+            request = _init_request(environ)
+        else:
+            request = cherrypy.request
+        try:
+            
+            env = environ.get
+            # 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
+            
+            path = environ.get('SCRIPT_NAME', '') + environ.get('PATH_INFO', '')
+            response = request.run(environ['REQUEST_METHOD'], path,
+                                   environ.get('QUERY_STRING'),
+                                   environ.get('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 cherrypy.config.get("throw_errors", False):
+                raise
+            tb = format_exc()
+            cherrypy.log(tb)
+            if not cherrypy.config.get("show_tracebacks", False):
+                tb = ""
+            s, h, b = bare_error(tb)
+            exc = sys.exc_info()
         
-        if app:
-            request.app = app
-        
-        path = environ.get('SCRIPT_NAME', '') + environ.get('PATH_INFO', '')
-        response = request.run(environ['REQUEST_METHOD'], path,
-                               environ.get('QUERY_STRING'),
-                               environ.get('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:
+            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)
-        request = None
-        raise ex
-    except:
-        if cherrypy.config.get("throw_errors", False):
-            raise
-        tb = format_exc()
-        cherrypy.log(tb)
-        if not cherrypy.config.get("show_tracebacks", False):
-            tb = ""
-        s, h, b = bare_error(tb)
-        exc = sys.exc_info()
+            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
     
-    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
+    def merge(self, conf):
+        """Merge the given config into self.config."""
+        config.merge(self.conf, conf)
+        
+        # Create log handlers as specified in config.
+        rootconf = self.conf.get("/", {})
+        config._configure_builtin_logging(rootconf, self.access_log, "log_access_file")
+        config._configure_builtin_logging(rootconf, self.error_log)
+    
+    def guess_abs_path(self):
+        """Guess the absolute URL from server.socket_host and script_name.
+        
+        When inside a request, the abs_path can be formed via:
+            cherrypy.request.base + (cherrypy.request.app.script_name or "/")
+        
+        However, outside of the request we must guess, hoping the deployer
+        set socket_host and socket_port correctly.
+        """
+        port = int(config.get('server.socket_port', 80))
+        if port in (443, 8443):
+            scheme = "https://"
+        else:
+            scheme = "http://"
+        host = config.get('server.socket_host', '')
+        if port != 80:
+            host += ":%s" % port
+        return scheme + host + self.script_name
 
-def wsgiApp(environ, start_response):
-    """The WSGI 'application object' for CherryPy.
+class HostedWSGI(object):
+    def __init__(self, app):
+        self.app = app
+        self._cp_config = {'tools.wsgiapp.on': True,
+                           'tools.wsgiapp.app': app,
+                          }
     
-    Use this as the same WSGI callable for all your CP apps.
-    """
-    return _wsgi_callable(environ, start_response)
-
-def make_app(app):
-    """Factory for making separate WSGI 'application objects' for each CP app.
-    
-    Example:
-        # 'app' will be a CherryPy application object
-        app = cherrypy.tree.mount(Root(), "/", localconf)
-        
-        # 'wsgi_app' will be a WSGI application
-        wsgi_app = _cpwsgi.make_app(app)
-    """
-    def single_app(environ, start_response):
-        return _wsgi_callable(environ, start_response, app)
-    return single_app
-
-
+    def __call__(self, environ, start_response):
+        return self.app(environ, start_response)
 
 #                            Server components                            #
 
             bind_addr = (conf('server.socket_host'),
                          conf('server.socket_port'))
         
-        apps = [(base, wsgiApp) for base in cherrypy.tree.apps]
+        app = cherrypy.tree.dispatch
         
         s = _cpwsgiserver.CherryPyWSGIServer
-        s.__init__(self, bind_addr, apps,
+        s.__init__(self, bind_addr, app,
                    conf('server.thread_pool'),
                    conf('server.socket_host'),
                    request_queue_size = conf('server.socket_queue_size'),
         atoms = [unquote(x) for x in quoted_slash.split(path)]
         path = "%2F".join(atoms)
         
-        for mount_point, wsgi_app in self.server.mount_points:
-            if path == "*":
-                # This means, of course, that the first wsgi_app will
-                # always handle a URI of "*".
-                self.environ["SCRIPT_NAME"] = ""
-                self.environ["PATH_INFO"] = "*"
-                self.wsgi_app = wsgi_app
-                break
-            # The mount_points list should be sorted by length, descending.
-            if path.startswith(mount_point):
-                self.environ["SCRIPT_NAME"] = mount_point
-                self.environ["PATH_INFO"] = path[len(mount_point):]
-                self.wsgi_app = wsgi_app
-                break
-        else:
-            self.abort("404 Not Found")
-            return
+        self.wsgi_app = self.server.app
+        self.environ['SCRIPT_NAME'] = ''
+        self.environ['PATH_INFO'] = path
         
         # Note that, like wsgiref and most other WSGI servers,
         # we unquote the path but not the query string.
                  max=-1, request_queue_size=5, timeout=10):
         self.requests = Queue.Queue(max)
         
-        if callable(wsgi_app):
-            # We've been handed a single wsgi_app, in CP-2.1 style.
-            # Assume it's mounted at "".
-            self.mount_points = [("", wsgi_app)]
-        else:
-            # We've been handed a list of (mount_point, wsgi_app) tuples,
-            # so that the server can call different wsgi_apps, and also
-            # correctly set SCRIPT_NAME.
-            self.mount_points = wsgi_app
-        self.mount_points.sort()
-        self.mount_points.reverse()
+        self.app = wsgi_app
         
         self.bind_addr = bind_addr
         self.numthreads = numthreads or 1
         if setup:
             setup()
         
-        # The setup functions probably mounted new apps.
-        # Tell our server about them.
-        apps = []
-        for base, app in cherrypy.tree.apps.iteritems():
-            if base == "/":
-                base = ""
-            if conf.get("profiling.on", False):
-                apps.append((base, profiler.make_app(_cpwsgi.wsgiApp)))
-##                apps.append((base, profiler.make_app(_cpwsgi.wsgiApp, aggregate=True)))
-            else:
-                apps.append((base, _cpwsgi.wsgiApp))
-##            # We could use the following line, but it breaks test_tutorials
-##            apps.append((base, _cpwsgi.make_app(app)))
-        apps.sort()
-        apps.reverse()
         for s in cherrypy.server.httpservers:
-            s.mount_points = apps
+            s.app = cherrypy.tree.dispatch
         
         suite = CPTestLoader.loadTestsFromName(testmod)
         CPTestRunner.run(suite)
         'test_sessionauthenticate',
 ##        'test_states',
         'test_xmlrpc',
-        'test_wsgiapp',
+        'test_wsgiapps',
     ]
     CommandLineParser(testList).run()
     

test/test_wsgiapp.py

-import test
-test.prefer_parent_path()
-
-
-def setup_server():
-    import os
-    curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
-    
-    import cherrypy
-    
-    def test_app(environ, start_response):
-        status = '200 OK'
-        response_headers = [('Content-type', 'text/plain')]
-        start_response(status, response_headers)
-        yield 'Hello, world!\n'
-        yield 'This is a wsgi app running within CherryPy!\n\n'
-        keys = environ.keys()
-        keys.sort()
-        for k in keys:
-            yield '%s: %s\n' % (k,environ[k])
-    
-    class Root:
-        def index(self):
-            return "I'm a regular CherryPy page handler!"
-        index.exposed = True
-    
-    
-    class HostedWSGI(object):
-        _cp_config = {'tools.wsgiapp.on': True,
-                      'tools.wsgiapp.app': test_app,
-                      }
-    
-    cherrypy.config.update({'log_to_screen': False,
-                            'environment': 'production',
-                            'show_tracebacks': True,
-                            })
-    cherrypy.tree.mount(Root())
-    
-    conf0 = {'/static': {'tools.staticdir.on': True,
-                         'tools.staticdir.root': curdir,
-                         'tools.staticdir.dir': 'static',
-                         }}
-    cherrypy.tree.mount(HostedWSGI(), '/hosted/app0', conf0)
-
-
-import helper
-
-
-class WSGIAppTest(helper.CPWebCase):
-    
-    wsgi_output = '''Hello, world!
-This is a wsgi app running within CherryPy!'''
-
-    def test_01_standard_app(self):
-        self.getPage("/")
-        self.assertBody("I'm a regular CherryPy page handler!")
-    
-    def test_02_tools(self):
-        self.getPage("/hosted/app0")
-        self.assertHeader("Content-Type", "text/plain")
-        self.assertInBody(self.wsgi_output)
-    
-    def test_04_static_subdir(self):
-        self.getPage("/hosted/app0/static/index.html")
-        self.assertStatus('200 OK')
-        self.assertHeader('Content-Type', 'text/html')
-        self.assertBody('Hello, world\r\n')
-
-if __name__ == '__main__':
-    setup_server()
-    helper.testmain()
-

test/test_wsgiapps.py

+import test
+test.prefer_parent_path()
+
+
+def setup_server():
+    import os
+    curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+    
+    import cherrypy
+    
+    def test_app(environ, start_response):
+        status = '200 OK'
+        response_headers = [('Content-type', 'text/plain')]
+        start_response(status, response_headers)
+        yield 'Hello, world!\n'
+        yield 'This is a wsgi app running within CherryPy!\n\n'
+        keys = environ.keys()
+        keys.sort()
+        for k in keys:
+            yield '%s: %s\n' % (k,environ[k])
+
+    def reversing_middleware(app):
+        def _app(environ, start_response):
+            results = app(environ, start_response)
+            if not isinstance(results, basestring):
+                results = "".join(results)
+            results = list(results)
+            results.reverse()
+            return "".join(results)
+        return _app
+    
+    class Root:
+        def index(self):
+            return "I'm a regular CherryPy page handler!"
+        index.exposed = True
+    
+    
+    class HostedWSGI(object):
+        _cp_config = {'tools.wsgiapp.on': True,
+                      'tools.wsgiapp.app': test_app,
+                      }
+    
+    cherrypy.config.update({'log_to_screen': False,
+                            'environment': 'production',
+                            'show_tracebacks': True,
+                            })
+    cherrypy.tree.mount(Root())
+    
+    conf0 = {'/static': {'tools.staticdir.on': True,
+                         'tools.staticdir.root': curdir,
+                         'tools.staticdir.dir': 'static',
+                         }}
+    cherrypy.tree.mount(HostedWSGI(), '/hosted/app0', conf0)
+    cherrypy.tree.mount(test_app, '/hosted/app1', wrap=False)
+    
+    app = reversing_middleware(cherrypy.Application(Root()))
+    cherrypy.tree.mount(app, '/hosted/app2', wrap=False)
+
+import helper
+
+
+class WSGIAppTest(helper.CPWebCase):
+    
+    wsgi_output = '''Hello, world!
+This is a wsgi app running within CherryPy!'''
+
+    def test_01_standard_app(self):
+        self.getPage("/")
+        self.assertBody("I'm a regular CherryPy page handler!")
+    
+    def test_02_wrapped_wsgi(self):
+        self.getPage("/hosted/app0")
+        self.assertHeader("Content-Type", "text/plain")
+        self.assertInBody(self.wsgi_output)
+    
+    def test_03_static_subdir(self):
+        self.getPage("/hosted/app0/static/index.html")
+        self.assertStatus('200 OK')
+        self.assertHeader('Content-Type', 'text/html')
+        self.assertBody('Hello, world\r\n')
+    
+    def test_04_pure_wsgi(self):
+        self.getPage("/hosted/app1")
+        self.assertHeader("Content-Type", "text/plain")
+        self.assertInBody(self.wsgi_output)
+
+    def test_05_wrapped_cp_app(self):
+        self.getPage("/hosted/app2/")
+        body = list("I'm a regular CherryPy page handler!")
+        body.reverse()
+        body = "".join(body)
+        self.assertInBody(body)
+
+if __name__ == '__main__':
+    setup_server()
+    helper.testmain()
+
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.