Issue #568 resolved

Allow arbitrary virtual path arrangements

Robert Brewer
created an issue

By default, CherryPy currently looks up a handler by traversing the Application's object tree; when it finds a candidate, it takes any remaining path atoms and sends them to the handler as positional arguments. For example, given the URL {{{/path/to/handler/2006/08/04/title}}}, handler might be defined as: {{{def handler(year, month, day, title)}}}.

It would be helpful to some architectures if those "virtual path components" could be gathered from atoms within a URL, and not just from the "leftover" path atoms, so that one might write {{{/features/2006/08/04/handler/title}}} if needed.

From http://www.defuze.org/oss/cpirc/%23cherrypy.2006-04-06.log.html:

{{{ doc sounds reasonable, ok now I got that problem that I have something like /myblog/<month>/some/more/type/<id>/action 11:55 doc I solve the <id> thing with default too 11:55 uche I do think that's too limited 11:55 doc so kind of when I solve <month> with default, I cannot benefit from that anymore 11:55 uche CP should make it easier to re-chain to the standard pathe algorithm 11:55 uche I still guess there's an internal call you could make to do this 11:56 uche but I agree it should not be an internal call 11:56 uche should be public API 11:56 doc ok, it's a bit more tricky then the month example 11:57 doc it's more like /myblog/some/more -> /myblog/<current-month>/some/more 11:58 uche well, that still requires the same base capability 11:59 doc so I kinda would need a "def some" controller doing the redirect to /myblog/$name/some/more 11:59 doc (I sort of also deal with issues of plugins to my app that need to override some pieces of the path to implement their own thing, customer custom requirements, that sorta thing) 12:04 doc somebody should think about how to dynamically dispatch Restfull path components with cherrypy from an API/Pattern perspective, so users don't have to run in circles to figure that stuff out. 12:06 uche CP already has an algorithm: map "/a/b/c" to object foo.c() 12:09 uche all it would be is a simple: 12:09 uche cp_handle_path(path, root) 12:09 uche so that in the handler for /a, you could call cp_handle_path('b/c', self) 12:10 uche that would address doc's need easily 12:10 doc of course it would be kinda nice when cherrypy solved that of my problems, but I do also think that it'd help users to adopt restfull programming patterns, and restfull is where that request comes from. 12:15 uche and to be honest, CP does make me have to do a lot of path manageemnt magic that I feel I shouldn't have to be doing 12:17 uche 1) I think that it's important with the new CP documentation to include a recipe book for URL patterns (RESTful & beyond) 12:21 uche 2) I think that one of the things that needs to be addressed is to expose CP's internal pathe traversal system for user-level code 12:22 uche 3) I think that REST-driven patterns are a special case of (1) and deserve their own section, in bold 12:22 Lawouach 2) I think it would be much better to first make sure the algorithm to be external enough so that it can be echanged or seen as a library call 12:23 uche Lawouach, I would say that if you can't do that, the design might need to be fixed 12:23 uche the core of path resolution should be an opaque operation 12:24 uche separation of concerns, and all that 12:24 }}}

By posting the above, I'm not saying cp_handle_path is the "right way" to do this. But stating the need in data-oriented terms (and avoiding ambiguous phrases like "REST-driven patterns") might actually result in some work being done. ;)

Comments (11)

  1. michele

    The dispatcher I proposed in ticket #554 makes these type of things really really easy, take a look:

    class Features:
        _cp_mappings = [
            ('/{year}/{month}/{day}/handler/{title}', 'handler')
        ]
    
        def handler(self, year, month, day, title)
          ...
    

    I would like to underline that the approach I've take falls back to the default CP dispatching mechanism if there are no mappings defined or matching the actual request, integrating my patch directly into CP will require really little effort compared to the patch I proposed (that at that time was using a custom (and wrong as we discussed on IRC at that time) Dispatcher class).

    Hooking this thing into _cprequest.Dispatch will just require this: - Use of SimpleParser (I've taken it from selector) - Analysing the request url starting from the right (as the default Dispatch already does) - Checking the presence of a _cp_mappings attribute and eventually checking the presence of a match, otherwise use the default mechanism - Merging the eventual match dict with request.params (easily done with LateParamPageHandler) - Merging the config as the default Dispatch is already does)

    I think that as said on #554 this is suitable for a cp-extra, but hooking it into CP default doesn't require many efforts and will really give many advantage to CP users (out of the box) and address a concern that is usually used to criticize CP. ;-)

    Just my 0.2€.

  2. Robert Brewer reporter

    I think this can be done more easily by collecting vpath atoms as we traverse, rather than parsing strings. For example:

    class Features:
        _cp_virtual_path = ['year', 'month', 'day']
    
        def handler1(self, year, month, day, title)
          ...
        
        def handler2(self, year, month, day, comment)
          ...
        
    

    Then multiple handler methods are candidates for the same leading vpath, without having to explicitly declare each one.

  3. Anonymous

    Maybe I'm completely off track but woudln't it be easier to add a Routes dispatcher as built-in in CP so that people can simply use Routes to do that.

    No offense but at first sight I find that both your ideas reproduce a behavior that Routes could provide. Except that in that case URLs would have to be explictely declared (unlike what fumanchu suggests however).

  4. Robert Brewer reporter

    I've attached a patch against trunk which allows intermediate vpath components. Example use in the test suite. Note that this scheme doesn't require the user to explicitly declare any of the virtual components--it simply collects components which don't map to objects on the tree, and passes all of them as positional args.

    Thoughts?

  5. Robert Brewer reporter

    The patch I posted has some problems. For example, if you have:

    class Directory:
    
        def default(self, id, part, command):
            if part == "address":
                return Address.default(id, command)
            ...
        
        def add(self):
            ...
    

    Then you might have expected the URL /directory/1/address/add to be handled by the "default" method; however, with the patch CP will call Directory.add(1, "address"). So we either need a way to explicitly declare which components are virtual, or try some other approach.

  6. michele

    Hi fumanchu,

    I haven't got the opportunity to try out your patch yet. :-(

    Anyway, I think that the default object mapping dispatch CP uses is very intuitive and works well for most needs, that's why I still like my own solution more than the others.

    When I need a fancy url (rarely) I want to declare it explicitly and define *precisely* what's going to happen with it, your last solution works but is totally implicit and hidden making the dispatch mechanism unpredictable (as your last comment shows), it also adds an additional layer of complexity to newbies trying to understand the way CP dispatching works.

    I agree with you that there should be a way to explicitly which components are virtual but as I said on IRC some times ago I don't like the "_cp_virtual_path" solution as I find it limiting (you can only declare a set of components) and not intuitive (it took me something like 5 minutes to understand how it was supposed to work).

    Basically I think your solutions are somewhat magic and don't fit well with the way CP works that's very pythonic and predicable.

    OTOH my solution is quite intuitive since the mapping happens explicitly and it immediately shows without doubts (or mental re composition needed) what special url you're defining and who is going to handle it, moreover it also provides http method based dispatch.

    For example I would really like to be able to do something like this with CP: http://bitworking.org/news/wsgicollection

    Using my solution you can easily do it:

    class RecipeCollection:
        _cp_mappings = [
            ('/cookbook', GET='list', POST='create'),
            ('/cookbook/{id}', GET='retrieve', PUT'update', DELETE='delete'),
            ('/cookbook/;create_form', GET='create_form'),
            ('/cookbook/{id};comment_form', GET='comment_form')
        ]
    
        # GET /cookbook/
        def list(self):
            pass
    
        # POST /cookbook/
        def create(self, **params):
            pass
    
        # GET /cookbook/1
        def retrieve(self, id):
            pass
    
        # PUT /cookbook/1
        def update(self, id, **params):
            pass
    
        # DELETE /cookbook/1
        def delete(self, id):
            pass
    
        # GET /cookbook/;create_form
        def get_create_form(self):
            pass
    
        # POST /cookbook/1;comment_form
        def post_comment_form(self):
            pass
    

    Yes, your solutions are saving typing but in this case (and since you don't need such an explicit mapping every time) I vastly prefer being explicit and be sure that the dispatch will do what '''I expect and want it to do''' instead of '''letting him figure it out behind my back'''.

    I like the object mapping mechanism as it saves typing when you need a typical mapping but sometimes you need to get more fancy and here the Django way works well (but uses regex directly :-(), I think my solution brings together the best of both worlds. ;-)

    Finally, Routes is a great package but it uses a centralized configuration and the url is constructed upon some rules, Selector (as it's author says) is the simpler thing that works well, you don't need to teach yourself special rules of keywords and the way I've used it (just its parser) you attach mappings directly to the class they belong to.

    Just my 0.02€.

  7. Anonymous

    Unless anyone thinks it is critical I propose to remove this from the 3.0 milestone roadmap. This seems to require a little more discussion between us to decide whether or not we must include it into CP.

  8. Log in to comment