Commits

Anonymous committed aaa00bc

Merged in changes from python3: r2710 r2711 r2714 r2718 r2719 r2720 r2721 . cherrypy.popargs() and _cp_dispatch fixes.

  • Participants
  • Parent commits f4ac347

Comments (0)

Files changed (3)

cherrypy/__init__.py

         alias = func
         return expose_
 
+        
+def popargs(*args, **kwargs):
+    """A decorator for _cp_dispatch 
+    (cherrypy.dispatch.Dispatcher.dispatch_method_name).
+
+    Optional keyword argument: handler=(Object or Function)
+    
+    Provides a _cp_dispatch function that pops off path segments into 
+    cherrypy.request.params under the names specified.  The dispatch
+    is then forwarded on to the next vpath element.
+    
+    Note that any existing (and exposed) member function of the class that
+    popargs is applied to will override that value of the argument.  For
+    instance, if you have a method named "list" on the class decorated with
+    popargs, then accessing "/list" will call that function instead of popping
+    it off as the requested parameter.  This restriction applies to all 
+    _cp_dispatch functions.  The only way around this restriction is to create
+    a "blank class" whose only function is to provide _cp_dispatch.
+    
+    If there are path elements after the arguments, or more arguments
+    are requested than are available in the vpath, then the 'handler'
+    keyword argument specifies the next object to handle the parameterized
+    request.  If handler is not specified or is None, then self is used.
+    If handler is a function rather than an instance, then that function
+    will be called with the args specified and the return value from that
+    function used as the next object INSTEAD of adding the parameters to
+    cherrypy.request.args.
+    
+    This decorator may be used in one of two ways:
+    
+    As a class decorator:
+    @cherrypy.popargs('year', 'month', 'day')
+    class Blog:
+        def index(self, year=None, month=None, day=None):
+            #Process the parameters here; any url like
+            #/, /2009, /2009/12, or /2009/12/31
+            #will fill in the appropriate parameters.
+            
+        def create(self):
+            #This link will still be available at /create.  Defined functions
+            #take precedence over arguments.
+            
+    Or as a member of a class:
+    class Blog:
+        _cp_dispatch = cherrypy.popargs('year', 'month', 'day')
+        #...
+        
+    The handler argument may be used to mix arguments with built in functions.
+    For instance, the following setup allows different activities at the
+    day, month, and year level:
+    
+    class DayHandler:
+        def index(self, year, month, day):
+            #Do something with this day; probably list entries
+            
+        def delete(self, year, month, day):
+            #Delete all entries for this day
+            
+    @cherrypy.popargs('day', handler=DayHandler())
+    class MonthHandler:
+        def index(self, year, month):
+            #Do something with this month; probably list entries
+            
+        def delete(self, year, month):
+            #Delete all entries for this month
+            
+    @cherrypy.popargs('month', handler=MonthHandler())
+    class YearHandler:
+        def index(self, year):
+            #Do something with this year
+            
+        #...
+        
+    @cherrypy.popargs('year', handler=YearHandler())
+    class Root:
+        def index(self):
+            #...
+        
+    """
+
+    #Since keyword arg comes after *args, we have to process it ourselves
+    #for lower versions of python.
+
+    handler = None
+    handler_call = False
+    for k,v in kwargs.items():
+        if k == 'handler':
+            handler = v
+        else:
+            raise TypeError(
+                "cherrypy.popargs() got an unexpected keyword argument '{0}'" \
+                .format(k)
+                )
+
+    import inspect
+
+    if handler is not None \
+        and (hasattr(handler, '__call__') or inspect.isclass(handler)):
+        handler_call = True
+    
+    def decorated(cls_or_self=None, vpath=None):
+        if inspect.isclass(cls_or_self):
+            #cherrypy.popargs is a class decorator
+            cls = cls_or_self
+            setattr(cls, dispatch.Dispatcher.dispatch_method_name, decorated)
+            return cls
+        
+        #We're in the actual function
+        self = cls_or_self
+        parms = {}
+        for arg in args:
+            if not vpath:
+                break
+            parms[arg] = vpath.pop(0)
+                
+        if handler is not None:
+            if handler_call:
+                return handler(**parms)
+            else:
+                request.params.update(parms)
+                return handler
+                
+        request.params.update(parms)
+            
+        #If we are the ultimate handler, then to prevent our _cp_dispatch
+        #from being called again, we will resolve remaining elements through
+        #getattr() directly.
+        if vpath:
+            return getattr(self, vpath.pop(0), None)
+        else:
+            return self
+        
+    return decorated
 
 def url(path="", qs="", script_name=None, base=None, relative=None):
     """Create an absolute URL for the given path.

cherrypy/_cpdispatch.py

         dispatch_name = self.dispatch_method_name
         
         # Get config for the root object/path.
-        curpath = ""
+        fullpath = [x for x in path.strip('/').split('/') if x] + ['index']
+        fullpath_len = len(fullpath)
+        segleft = fullpath_len
         nodeconf = {}
         if hasattr(root, "_cp_config"):
             nodeconf.update(root._cp_config)
         if "/" in app.config:
             nodeconf.update(app.config["/"])
-        object_trail = [['root', root, nodeconf, curpath]]
+        object_trail = [['root', root, nodeconf, segleft]]
         
         node = root
-        names = [x for x in path.strip('/').split('/') if x] + ['index']
-        iternames = names[:]
+        iternames = fullpath[:]
         while iternames:
             name = iternames[0]
             # map to legal Python identifiers (e.g. replace '.' with '_')
             
             nodeconf = {}
             subnode = getattr(node, objname, None)
+            pre_len = len(iternames)
             if subnode is None:
                 dispatch = getattr(node, dispatch_name, None)
                 if dispatch and callable(dispatch) and not \
                         getattr(dispatch, 'exposed', False):
+                    #Don't expose the hidden 'index' token to _cp_dispatch
+                    index_name = iternames.pop()
                     subnode = dispatch(vpath=iternames)
-            name = iternames.pop(0)
+                    iternames.append(index_name)
+                else:
+                    #We didn't find a path, but keep processing in case there
+                    #is a default() handler.
+                    iternames.pop(0)
+            else:
+                #We found the path, remove the vpath entry
+                iternames.pop(0)
+            segleft = len(iternames)
+            if segleft > pre_len:
+                #No path segment was removed.  Raise an error.
+                raise cherrypy.CherryPyException(
+                    "A vpath segment was added.  Custom dispatchers may only "
+                    + "remove elements.  While trying to process "
+                    + "{0} in {1}".format(name, fullpath)
+                    )
+            elif segleft == pre_len:
+                #Assume that the handler used the current path segment, but
+                #did not pop it.  This allows things like 
+                #return getattr(self, vpath[0], None)
+                iternames.pop(0)
+                segleft -= 1
             node = subnode
 
             if node is not None:
                     nodeconf.update(node._cp_config)
             
             # Mix in values from app.config for this path.
-            curpath = "/".join((curpath, name))
-            if curpath in app.config:
-                nodeconf.update(app.config[curpath])
+            existing_len = fullpath_len - pre_len
+            if existing_len != 0:
+                curpath = '/' + '/'.join(fullpath[0:existing_len])
+            else:
+                curpath = ''
+            new_segs = fullpath[fullpath_len - pre_len:fullpath_len - segleft]
+            for seg in new_segs:
+                curpath += '/' + seg
+                if curpath in app.config:
+                    nodeconf.update(app.config[curpath])
             
-            object_trail.append([name, node, nodeconf, curpath])
-        
+            object_trail.append([name, node, nodeconf, segleft])
+            
         def set_conf():
             """Collapse all object_trail config into cherrypy.request.config."""
             base = cherrypy.config.copy()
             # Note that we merge the config from each node
             # even if that node was None.
-            for name, obj, conf, curpath in object_trail:
+            for name, obj, conf, segleft in object_trail:
                 base.update(conf)
                 if 'tools.staticdir.dir' in conf:
-                    base['tools.staticdir.section'] = curpath
+                    base['tools.staticdir.section'] = '/' + '/'.join(fullpath[0:fullpath_len - segleft])
             return base
         
         # Try successive objects (reverse order)
         num_candidates = len(object_trail) - 1
         for i in range(num_candidates, -1, -1):
             
-            name, candidate, nodeconf, curpath = object_trail[i]
+            name, candidate, nodeconf, segleft = object_trail[i]
             if candidate is None:
                 continue
             
                 if getattr(defhandler, 'exposed', False):
                     # Insert any extra _cp_config from the default handler.
                     conf = getattr(defhandler, "_cp_config", {})
-                    object_trail.insert(i+1, ["default", defhandler, conf, curpath])
+                    object_trail.insert(i+1, ["default", defhandler, conf, segleft])
                     request.config = set_conf()
                     # See http://www.cherrypy.org/ticket/613
                     request.is_index = path.endswith("/")
-                    return defhandler, names[i:-1]
+                    return defhandler, fullpath[fullpath_len - segleft:-1]
             
             # Uncomment the next line to restrict positional params to "default".
             # if i < num_candidates - 2: continue
                     # Note that this also includes handlers which take
                     # positional parameters (virtual paths).
                     request.is_index = False
-                return candidate, names[i:-1]
+                return candidate, fullpath[fullpath_len - segleft:-1]
         
         # We didn't find anything
         request.config = set_conf()

cherrypy/test/test_dynamicobjectmapping.py

 script_names = ["", "/foo", "/users/fred/blog", "/corp/blog"]
 
 
+
+def setup_server():
+    class SubSubRoot:
+        def index(self):
+            return "SubSubRoot index"
+        index.exposed = True
+
+        def default(self, *args):
+            return "SubSubRoot default"
+        default.exposed = True
+
+        def handler(self):
+            return "SubSubRoot handler"
+        handler.exposed = True
+
+        def dispatch(self):
+            return "SubSubRoot dispatch"
+        dispatch.exposed = True
+
+    subsubnodes = {
+        '1': SubSubRoot(),
+        '2': SubSubRoot(),
+    }
+
+    class SubRoot:
+        def index(self):
+            return "SubRoot index"
+        index.exposed = True
+
+        def default(self, *args):
+            return "SubRoot %s" % (args,)
+        default.exposed = True
+
+        def handler(self):
+            return "SubRoot handler"
+        handler.exposed = True
+
+        def _cp_dispatch(self, vpath):
+            return subsubnodes.get(vpath[0], None)
+
+    subnodes = {
+        '1': SubRoot(),
+        '2': SubRoot(),
+    }
+    class Root:
+        def index(self):
+            return "index"
+        index.exposed = True
+
+        def default(self, *args):
+            return "default %s" % (args,)
+        default.exposed = True
+
+        def handler(self):
+            return "handler"
+        handler.exposed = True
+
+        def _cp_dispatch(self, vpath):
+            return subnodes.get(vpath[0])
+
+    #--------------------------------------------------------------------------
+    # DynamicNodeAndMethodDispatcher example.
+    # This example exposes a fairly naive HTTP api
+    class User(object):
+        def __init__(self, id, name):
+            self.id = id
+            self.name = name
+
+        def __unicode__(self):
+            return unicode(self.name)
+
+    user_lookup = {
+        1: User(1, 'foo'),
+        2: User(2, 'bar'),
+    }
+
+    def make_user(name, id=None):
+        if not id:
+            id = max(*user_lookup.keys()) + 1
+        user_lookup[id] = User(id, name)
+        return id
+
+    class UserContainerNode(object):
+        exposed = True
+
+        def POST(self, name):
+            """
+            Allow the creation of a new Object
+            """
+            return "POST %d" % make_user(name)
+
+        def GET(self):
+            keys = user_lookup.keys()
+            keys.sort()
+            return unicode(keys)
+
+        def dynamic_dispatch(self, vpath):
+            try:
+                id = int(vpath[0])
+            except (ValueError, IndexError):
+                return None
+            return UserInstanceNode(id)
+
+    class UserInstanceNode(object):
+        exposed = True
+        def __init__(self, id):
+            self.id = id
+            self.user = user_lookup.get(id, None)
+
+            # For all but PUT methods there MUST be a valid user identified
+            # by self.id
+            if not self.user and cherrypy.request.method != 'PUT':
+                raise cherrypy.HTTPError(404)
+
+        def GET(self, *args, **kwargs):
+            """
+            Return the appropriate representation of the instance.
+            """
+            return unicode(self.user)
+
+        def POST(self, name):
+            """
+            Update the fields of the user instance.
+            """
+            self.user.name = name
+            return "POST %d" % self.user.id
+
+        def PUT(self, name):
+            """
+            Create a new user with the specified id, or edit it if it already exists
+            """
+            if self.user:
+                # Edit the current user
+                self.user.name = name
+                return "PUT %d" % self.user.id
+            else:
+                # Make a new user with said attributes.
+                return "PUT %d" % make_user(name, self.id)
+
+        def DELETE(self):
+            """
+            Delete the user specified at the id.
+            """
+            id = self.user.id
+            del user_lookup[self.user.id]
+            del self.user
+            return "DELETE %d" % id
+
+    
+    class ABHandler:
+        class CustomDispatch:
+            def index(self, a, b):
+                return "custom"
+            index.exposed = True
+                
+        def _cp_dispatch(self, vpath):
+            """Make sure that if we don't pop anything from vpath,
+            processing still works.
+            """
+            return self.CustomDispatch()
+        
+        def index(self, a, b=None):
+            body = [ 'a:' + str(a) ]
+            if b is not None:
+                body.append(',b:' + str(b))
+            return ''.join(body)
+        index.exposed = True
+            
+        def delete(self, a, b):
+            return 'deleting ' + str(a) + ' and ' + str(b)
+        delete.exposed = True
+            
+    class IndexOnly:
+        def _cp_dispatch(self, vpath):
+            """Make sure that popping ALL of vpath still shows the index 
+            handler.
+            """
+            while vpath:
+                vpath.pop()
+            return self
+            
+        def index(self):
+            return "IndexOnly index"
+        index.exposed = True
+    
+    class DecoratedPopArgs:
+        """Test _cp_dispatch with @cherrypy.popargs."""
+        def index(self):
+            return "no params"
+        index.exposed = True
+        
+        def hi(self):
+            return "hi was not interpreted as 'a' param"
+        hi.exposed = True
+    DecoratedPopArgs = cherrypy.popargs('a', 'b', handler=ABHandler())(DecoratedPopArgs)
+            
+    class NonDecoratedPopArgs:
+        """Test _cp_dispatch = cherrypy.popargs()"""
+        
+        _cp_dispatch = cherrypy.popargs('a')
+        
+        def index(self, a):
+            return "index: " + str(a)
+        index.exposed = True
+            
+    class ParameterizedHandler:
+        """Special handler created for each request"""
+        
+        def __init__(self, a):
+            self.a = a
+            
+        def index(self):
+            if 'a' in cherrypy.request.params:
+                raise Exception("Parameterized handler argument ended up in request.params")
+            return self.a
+        index.exposed = True
+            
+    class ParameterizedPopArgs:
+        """Test cherrypy.popargs() with a function call handler"""
+    ParameterizedPopArgs = cherrypy.popargs('a', handler=ParameterizedHandler)(ParameterizedPopArgs)
+            
+    Root.decorated = DecoratedPopArgs()
+    Root.undecorated = NonDecoratedPopArgs()
+    Root.index_only = IndexOnly()
+    Root.parameter_test = ParameterizedPopArgs()
+
+    Root.users = UserContainerNode()
+
+    md = cherrypy.dispatch.MethodDispatcher('dynamic_dispatch')
+    for url in script_names:
+        conf = {'/': {
+                    'user': (url or "/").split("/")[-2],
+                },
+                '/users': {
+                    'request.dispatch': md
+                },
+            }
+        cherrypy.tree.mount(Root(), url, conf)
+
 class DynamicObjectMappingTest(helper.CPWebCase):
-    @staticmethod
-    def setup_server():
-        class SubSubRoot:
-            def index(self):
-                return "SubSubRoot index"
-            index.exposed = True
-
-            def default(self, *args):
-                return "SubSubRoot default"
-            default.exposed = True
-
-            def handler(self):
-                return "SubSubRoot handler"
-            handler.exposed = True
-
-            def dispatch(self):
-                return "SubSubRoot dispatch"
-            dispatch.exposed = True
-
-        subsubnodes = {
-            '1': SubSubRoot(),
-            '2': SubSubRoot(),
-        }
-
-        class SubRoot:
-            def index(self):
-                return "SubRoot index"
-            index.exposed = True
-
-            def default(self, *args):
-                return "SubRoot %s" % (args,)
-            default.exposed = True
-
-            def handler(self):
-                return "SubRoot handler"
-            handler.exposed = True
-
-            def _cp_dispatch(self, vpath):
-                return subsubnodes.get(vpath[0], None)
-
-        subnodes = {
-            '1': SubRoot(),
-            '2': SubRoot(),
-        }
-        class Root:
-            def index(self):
-                return "index"
-            index.exposed = True
-
-            def default(self, *args):
-                return "default %s" % (args,)
-            default.exposed = True
-
-            def handler(self):
-                return "handler"
-            handler.exposed = True
-
-            def _cp_dispatch(self, vpath):
-                return subnodes.get(vpath[0])
-
-        #--------------------------------------------------------------------------
-        # DynamicNodeAndMethodDispatcher example.
-        # This example exposes a fairly naive HTTP api
-        class User(object):
-            def __init__(self, id, name):
-                self.id = id
-                self.name = name
-
-            def __unicode__(self):
-                return unicode(self.name)
-
-        user_lookup = {
-            1: User(1, 'foo'),
-            2: User(2, 'bar'),
-        }
-
-        def make_user(name, id=None):
-            if not id:
-                id = max(*user_lookup.keys()) + 1
-            user_lookup[id] = User(id, name)
-            return id
-
-        class UserContainerNode(object):
-            exposed = True
-
-            def POST(self, name):
-                """
-                Allow the creation of a new Object
-                """
-                return "POST %d" % make_user(name)
-
-            def GET(self):
-                keys = user_lookup.keys()
-                keys.sort()
-                return unicode(keys)
-
-            def dynamic_dispatch(self, vpath):
-                try:
-                    id = int(vpath[0])
-                except ValueError:
-                    return None
-                return UserInstanceNode(id)
-
-        class UserInstanceNode(object):
-            exposed = True
-            def __init__(self, id):
-                self.id = id
-                self.user = user_lookup.get(id, None)
-
-                # For all but PUT methods there MUST be a valid user identified
-                # by self.id
-                if not self.user and cherrypy.request.method != 'PUT':
-                    raise cherrypy.HTTPError(404)
-
-            def GET(self, *args, **kwargs):
-                """
-                Return the appropriate representation of the instance.
-                """
-                return unicode(self.user)
-
-            def POST(self, name):
-                """
-                Update the fields of the user instance.
-                """
-                self.user.name = name
-                return "POST %d" % self.user.id
-
-            def PUT(self, name):
-                """
-                Create a new user with the specified id, or edit it if it already exists
-                """
-                if self.user:
-                    # Edit the current user
-                    self.user.name = name
-                    return "PUT %d" % self.user.id
-                else:
-                    # Make a new user with said attributes.
-                    return "PUT %d" % make_user(name, self.id)
-
-            def DELETE(self):
-                """
-                Delete the user specified at the id.
-                """
-                id = self.user.id
-                del user_lookup[self.user.id]
-                del self.user
-                return "DELETE %d" % id
-
-
-        Root.users = UserContainerNode()
-
-        md = cherrypy.dispatch.MethodDispatcher('dynamic_dispatch')
-        for url in script_names:
-            conf = {'/': {
-                        'user': (url or "/").split("/")[-2],
-                    },
-                    '/users': {
-                        'request.dispatch': md
-                    },
-                }
-            cherrypy.tree.mount(Root(), url, conf)
-
+    setup_server = staticmethod(setup_server)
 
     def testObjectMapping(self):
         for url in script_names:
         self.getPage("/users")
         self.assertBody("[]")
         self.assertHeader('Allow', 'GET, HEAD, POST')
+        
+    def testVpathDispatch(self):
+        self.getPage("/decorated/")
+        self.assertBody("no params")
+        
+        self.getPage("/decorated/hi")
+        self.assertBody("hi was not interpreted as 'a' param")
+        
+        self.getPage("/decorated/yo/")
+        self.assertBody("a:yo")
+        
+        self.getPage("/decorated/yo/there/")
+        self.assertBody("a:yo,b:there")
+        
+        self.getPage("/decorated/yo/there/delete")
+        self.assertBody("deleting yo and there")
+        
+        self.getPage("/decorated/yo/there/handled_by_dispatch/")
+        self.assertBody("custom")
+        
+        self.getPage("/undecorated/blah/")
+        self.assertBody("index: blah")
+        
+        self.getPage("/index_only/a/b/c/d/e/f/g/")
+        self.assertBody("IndexOnly index")
+        
+        self.getPage("/parameter_test/argument2/")
+        self.assertBody("argument2")