Commits

Robert Brewer  committed 8c6b486

Better support for custom toolboxes and namespaces:

1. Each Toolbox is now its own config namespace handler, and self-registers as such.
2. The global, app, and request contexts now each allow (but do not force) config namespace handlers to be PEP 343-style context managers, with {{{__enter__}}} and {{{__exit__}}} methods.
3. Each Toolbox and Tool has a new "namespace" attribute. Each Tool automatically inherits the namespace attribute of its Toolbox when attached.
4. Request.toolmap has been replaced with "toolmaps", a dict of toolmap dicts.
5. Request.configure was reduced to a one-liner and has therefore been removed.

Custom Toolbox usage is now a one-liner, e.g.: {{{mytools = cherrypy._cptools.Toolbox("mine")}}}, where "mine" will be the name of the new (request-scoped) config namespace.

  • Participants
  • Parent commits a17594f

Comments (0)

Files changed (6)

File cherrypy/__init__.py

 from cherrypy._cperror import HTTPError, HTTPRedirect, InternalRedirect, NotFound, CherryPyException
 from cherrypy._cperror import TimeoutError
 
+from cherrypy import _cprequest
+from cherrypy import _cpengine
+engine = _cpengine.Engine()
+
 from cherrypy import _cptools
 tools = _cptools.default_toolbox
 Tool = _cptools.Tool
 tree = _cptree.Tree()
 from cherrypy._cptree import Application
 from cherrypy import _cpwsgi as wsgi
-from cherrypy import _cpengine
-engine = _cpengine.Engine()
 from cherrypy import _cpserver
 server = _cpserver.Server()
 
 # Create request and response object (the same objects will be used
 #   throughout the entire life of the webserver, but will redirect
 #   to the "_serving" object)
-from cherrypy import _cprequest
 from cherrypy.lib import http as _http
 request = _ThreadLocalProxy('request',
                             _cprequest.Request(_http.Host("localhost", 80),

File cherrypy/_cpconfig.py

 This special entry 'imports' other config entries from a template stored in
 cherrypy._cpconfig.environments[environment]. It only applies to the global
 config, and only when you use cherrypy.config.update.
+
+You can define your own namespaces to be called at the Global, Application,
+or Request level, by adding a named handler to cherrypy.config.namespaces,
+app.namespaces, or cherrypy.engine.request_class.namespaces. The name can
+be any string, and the handler must be either a callable or a context
+manager.
 """
 
 import ConfigParser
         base.setdefault(section, {}).update(value_map)
 
 
+def _call_namespaces(config, namespaces):
+    """Iterate through config and pass it to each namespace.
+    
+    'config' should be a flat dict, where keys use dots to separate
+    namespaces, and values are arbitrary.
+    'namespaces' should be a dict whose keys are strings and whose
+    values are namespace handlers.
+    
+    The first name in each config key is used to look up the corresponding
+    namespace handler. For example, a config entry of {'tools.gzip.on': v}
+    will call the 'tools' namespace handler with the args: ('gzip.on', v)
+    
+    Each handler may be a bare callable, or it may be a context manager
+    with __enter__ and __exit__ methods, in which case the __enter__
+    method should return the callable.
+    """
+    # Separate the given config into namespaces
+    ns_confs = {}
+    for k in config:
+        if "." in k:
+            ns, name = k.split(".", 1)
+            bucket = ns_confs.setdefault(ns, {})
+            bucket[name] = config[k]
+    
+    # I chose __enter__ and __exit__ so someday this could be
+    # rewritten using Python 2.5's 'with' statement:
+    # for ns, handler in namespaces.iteritems():
+    #     with handler as callable:
+    #         for k, v in ns_confs.get(ns, {}).iteritems():
+    #             callable(k, v)
+    for ns, handler in namespaces.iteritems():
+        exit = getattr(handler, "__exit__", None)
+        if exit:
+            callable = handler.__enter__()
+            no_exc = True
+            try:
+                try:
+                    for k, v in ns_confs.get(ns, {}).iteritems():
+                        callable(k, v)
+                except:
+                    # The exceptional case is handled here
+                    no_exc = False
+                    if exit is None:
+                        raise
+                    if not exit(*sys.exc_info()):
+                        raise
+                    # The exception is swallowed if exit() returns true
+            finally:
+                # The normal and non-local-goto cases are handled here
+                if no_exc and exit:
+                    exit(None, None, None)
+        else:
+            for k, v in ns_confs.get(ns, {}).iteritems():
+                handler(k, v)
+
+
 class Config(dict):
     """The 'global' configuration data for the entire CherryPy process."""
     
         if 'tools.staticdir.dir' in config:
             config['tools.staticdir.section'] = "global"
         
-        # Must use this idiom in order to hit our custom __setitem__.
-        for k, v in config.iteritems():
-            self[k] = v
+        dict.update(self, config)
+        _call_namespaces(config, self.namespaces)
     
     def __setitem__(self, k, v):
         dict.__setitem__(self, k, v)
-        
-        # Override object properties if specified in config.
-        atoms = k.split(".", 1)
-        namespace = atoms[0]
-        if namespace in self.namespaces:
-            self.namespaces[namespace](atoms[1], v)
+        _call_namespaces({k: v}, self.namespaces)
 
 
 obsolete = {

File cherrypy/_cprequest.py

 
 
 # Config namespace handlers
-def tools_namespace(k, v):
-    """Attach tools specified in config."""
-    toolname, arg = k.split(".", 1)
-    bucket = cherrypy.request.toolmap.setdefault(toolname, {})
-    bucket[arg] = v
 
 def hooks_namespace(k, v):
     """Attach bare hooks declared in config."""
     path_info = "/"
     app = None
     handler = None
-    toolmap = {}
+    toolmaps = {}
     config = None
     recursive_redirect = False
     is_index = None
     show_tracebacks = True
     throw_errors = False
     
-    namespaces = {"tools": tools_namespace,
-                  "hooks": hooks_namespace,
+    namespaces = {"hooks": hooks_namespace,
                   "request": request_namespace,
                   "response": response_namespace,
                   "error_page": error_page_namespace,
                     # Make a copy of the class hooks
                     self.hooks = self.__class__.hooks.copy()
                     self.get_resource(path_info)
-                    self.configure()
+                    cherrypy._cpconfig._call_namespaces(self.config, self.namespaces)
                     
                     self.hooks.run('on_start_resource')
                     
         # dispatch() should set self.handler and self.config
         dispatch(path)
     
-    def configure(self):
-        """Process self.config, populate self.toolmap and set up each tool."""
-        self.toolmap = tm = {}
-        
-        # Process config namespaces (including tools.*)
-        reqconf = self.config
-        for k in reqconf:
-            atoms = k.split(".", 1)
-            namespace = atoms[0]
-            if namespace in self.namespaces:
-                self.namespaces[namespace](atoms[1], reqconf[k])
-        
-        # Run tool._setup(conf) for each tool in the new toolmap.
-        tools = cherrypy.tools
-        for toolname in tm:
-            if tm[toolname].get("on", False):
-                tool = getattr(tools, toolname)
-                tool._setup()
-    
     def process_body(self):
         """Convert request.rfile into request.params (or request.body)."""
         # Guard against re-reading body (e.g. on InternalRedirect)

File cherrypy/_cptools.py

     help(tool.callable) should give you more information about this Tool.
     """
     
+    namespace = "tools"
+    
     def __init__(self, point, callable, name=None, priority=50):
         self._point = point
         self.callable = callable
             pass
     
     def _merged_args(self, d=None):
-        tm = cherrypy.request.toolmap
+        tm = cherrypy.request.toolmaps[self.namespace]
         if self._name in tm:
             conf = tm[self._name].copy()
         else:
             raise TypeError("The %r Tool does not accept positional "
                             "arguments; you must use keyword arguments."
                             % self._name)
-        def wrapper(f):
+        def tool_decorator(f):
             if not hasattr(f, "_cp_config"):
                 f._cp_config = {}
-            f._cp_config["tools." + self._name + ".on"] = True
+            subspace = self.namespace + "." + self._name + "."
+            f._cp_config[subspace + "on"] = True
             for k, v in kwargs.iteritems():
-                f._cp_config["tools." + self._name + "." + k] = v
+                f._cp_config[subspace + k] = v
             return f
-        return wrapper
+        return tool_decorator
     
     def _setup(self):
         """Hook this tool into cherrypy.request.
                 nav = tools.staticdir.handler(section="/nav", dir="nav",
                                               root=absDir)
         """
-        def wrapper(*a, **kw):
+        def handle_func(*a, **kw):
             handled = self.callable(*args, **self._merged_args(kwargs))
             if not handled:
                 raise cherrypy.NotFound()
             return cherrypy.response.body
-        wrapper.exposed = True
-        return wrapper
+        handle_func.exposed = True
+        return handle_func
     
     def _wrapper(self, **kwargs):
         if self.callable(**kwargs):
 
 class XMLRPCController(object):
     
+    # Note we're hard-coding this into the 'tools' namespace. We could do
+    # a huge amount of work to make it relocatable, but the only reason why
+    # would be if someone actually disabled the default_toolbox. Meh.
     _cp_config = {'tools.xmlrpc.on': True}
     
     def __call__(self, *vpath, **params):
          
         if subhandler and getattr(subhandler, "exposed", False):
             body = subhandler(*(vpath + rpcparams), **params)
-
+        
         else:
             # http://www.cherrypy.org/ticket/533
             # if a method is not found, an xmlrpclib.Fault should be returned
             # raising an exception here will do that; see
             # cherrypy.lib.xmlrpc.on_error
             raise Exception, 'method "%s" is not supported' % attr
-            
-        conf = cherrypy.request.toolmap.get("xmlrpc", {})
+        
+        conf = cherrypy.request.toolmaps['tools'].get("xmlrpc", {})
         _xmlrpc.respond(body,
                         conf.get('encoding', 'utf-8'),
                         conf.get('allow_none', 0))
 
 
 class Toolbox(object):
-    """A collection of Tools."""
+    """A collection of Tools.
+    
+    This object also functions as a config namespace handler for itself.
+    """
+    
+    def __init__(self, namespace):
+        self.namespace = namespace
+        cherrypy.engine.request_class.namespaces[namespace] = self
     
     def __setattr__(self, name, value):
         # If the Tool._name is None, supply it from the attribute name.
         if isinstance(value, Tool):
             if value._name is None:
                 value._name = name
+            value.namespace = self.namespace
         object.__setattr__(self, name, value)
+    
+    def __enter__(self):
+        cherrypy.request.toolmaps[self.namespace] = {}
+        return self
+    
+    def __call__(self, k, v):
+        """Populate request.toolmaps from tools specified in config."""
+        toolname, arg = k.split(".", 1)
+        map = cherrypy.request.toolmaps[self.namespace]
+        bucket = map.setdefault(toolname, {})
+        bucket[arg] = v
+    
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        """Run tool._setup() for each tool in our toolmap."""
+        map = cherrypy.request.toolmaps.get(self.namespace)
+        if map:
+            for name, settings in map.iteritems():
+                if settings.get("on", False):
+                    tool = getattr(self, name)
+                    tool._setup()
 
 
-default_toolbox = _d = Toolbox()
+default_toolbox = _d = Toolbox("tools")
 default_toolbox.session_auth = SessionAuthTool(cptools.session_auth)
 _d.proxy = Tool('before_request_body', cptools.proxy, priority=30)
 _d.response_headers = Tool('on_start_resource', cptools.response_headers)

File cherrypy/_cptree.py

         _cpconfig.merge(self.config, config)
         
         # Handle namespaces specified in config.
-        rootconf = self.config.get("/", {})
-        for k, v in rootconf.iteritems():
-            atoms = k.split(".", 1)
-            namespace = atoms[0]
-            if namespace in self.namespaces:
-                self.namespaces[namespace](atoms[1], v)
+        _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

File cherrypy/test/test_tools.py

 
 def setup_server():
     
+    # Put check_access in a custom toolbox with its own namespace
+    myauthtools = cherrypy._cptools.Toolbox("myauth")
+    
     def check_access():
         if not getattr(cherrypy.request, "login", None):
             raise cherrypy.HTTPError(401)
-    tools.check_access = cherrypy.Tool('before_request_body', check_access)
+    myauthtools.check_access = cherrypy.Tool('before_request_body', check_access)
     
     def numerify():
         def number_it(body):
         # @tools.check_access()
         def restricted(self):
             return "Welcome!"
-        restricted = tools.check_access()(restricted)
+        restricted = myauthtools.check_access()(restricted)
         
         def err_in_onstart(self):
             return "success!"