Robert Brewer avatar Robert Brewer committed 6f4be47

Fix for #559 (allow config of WSGI middleware) via cherrypy.wsgi.pipeline. Includes tests.

Comments (0)

Files changed (3)

cherrypy/_cpwsgi.py

 from cherrypy.lib import http as _http
 
 
+class pipeline(list):
+    """An ordered list of configurable WSGI middleware.
+    
+    self: a list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a
+        constructor that takes an initial, positional wsgiapp 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.
+    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)
+    
+    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
+    
+    def namespace_handler(self, k, v):
+        """Config handler for our 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__
+        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))
+
+
 
 #                            Server components                            #
 

cherrypy/test/test.py

 ##        'test_states',
         'test_xmlrpc',
         'test_wsgiapps',
+        'test_wsgi_ns',
     ]
     CommandLineParser(testList).run()
     

cherrypy/test/test_wsgi_ns.py

+from cherrypy.test import test
+test.prefer_parent_path()
+
+
+def setup_server():
+    
+    import cherrypy
+    
+    
+    class ChangeCase(object):
+        
+        def __init__(self, app, to=None):
+            self.app = app
+            self.to = to
+        
+        def __call__(self, environ, start_response):
+            res = ''.join(self.app(environ, start_response))
+            return [getattr(res, self.to)()]
+    
+    def replace(app, map={}):
+        def replace_app(environ, start_response):
+            for line in app(environ, start_response):
+                for k, v in map.iteritems():
+                    line = line.replace(k, v)
+                yield line
+        return replace_app
+    
+    class Root(object):
+        
+        def index(self):
+            return "HellO WoRlD!"
+        index.exposed = True
+    
+    
+    root_conf = {'wsgi.pipeline': [('replace', replace)],
+                 'wsgi.replace.map': {'L': 'X', 'l': 'r'},
+                 }
+    
+    cherrypy.config.update({'environment': 'test_suite'})
+    
+    app = cherrypy.Application(Root())
+    p = cherrypy.wsgi.pipeline(app, [('changecase', ChangeCase)])
+    p.config['changecase'] = {'to': 'upper'}
+    cherrypy.tree.mount(app, conf={'/': 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, conf={'/': root_conf})
+
+
+from cherrypy.test import helper
+
+
+class WSGI_Namespace_Test(helper.CPWebCase):
+    
+    def test_pipeline(self):
+        self.getPage("/")
+        # If body is "HEXXO WORXD!", the middleware was applied out of order.
+        self.assertBody("HERRO WORRD!")
+
+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.