Comparing and Combining Traversal and URL Dispatch

(adapted from Bayle Shank's contribution at https://github.com/bshanks/pyramid/commit/c73b462c9671b5f2c3be26cf088ee983952ab61a).

Here's is an example which compares URL dispatch to traversal.

Let's say we want to map

/hello/login to a function login in the file myapp/views.py

/hello/foo to a function foo in the file myapp/views.py

/hello/listDirectory to a function listHelloDirectory in the file myapp/views.py

/hello/subdir/listDirectory to a function listSubDirectory in the file myapp/views.py

With URL dispatch, we might have:

1config.add_route('helloLogin', '/hello/login')
2config.add_route('helloFoo', '/hello/foo')
3config.add_route('helloList', '/hello/listDirectory')
4config.add_route('list', '/hello/{subdir}/listDirectory')
5
6config.add_view('myapp.views.login', route_name='helloLogin')
7config.add_view('myapp.views.foo', route_name='helloFoo')
8config.add_view('myapp.views.listHelloDirectory', route_name='helloList')
9config.add_view('myapp.views.listSubDirectory', route_name='list')

When the listSubDirectory function from myapp/views.py is called, it can tell what the subdirectory's name was by checking request.matchdict['subdir']. This is about all you need to know for URL-dispatch-based apps.

With traversal, we have a more complex setup:

 1class MyResource(dict):
 2    def __init__(self, name, parent):
 3        self.__name__ = name
 4        self.__parent__ = parent
 5
 6class MySubdirResource(MyResource):
 7    def __init__(self, name, parent):
 8        self.__name__ = name
 9        self.__parent__ = parent
10
11    # returns a MyResource object when the key is the name
12    # of a subdirectory
13    def __getitem__(self, key):
14        return MySubdirResource(key, self)
15
16class MyHelloResource(MySubdirResource):
17    pass
18
19def myRootFactory(request):
20    rootResource = MyResource('', None)
21    helloResource = MyHelloResource('hello', rootResource)
22    rootResource['hello'] = helloResource
23    return rootResource
24
25config.add_view('myapp.views.login', name='login')
26config.add_view('myapp.views.foo', name='foo')
27config.add_view('myapp.views.listHelloDirectory', context=MyHelloResource,
28                name='listDirectory')
29config.add_view('myapp.views.listSubDirectory', name='listDirectory')

In the traversal example, when a request for /hello/@@login comes in, the framework calls myRootFactory(request), and gets back the root resource. It calls the MyResource instance's __getitem__('hello'), and gets back a MyHelloResource. We don't traverse the next path segment (@@login`), because the ``@@ means the text that follows it is an explicit view name, and traversal ends. The view name 'login' is mapped to the login function in myapp/views.py, so this view callable is invoked.

When a request for /hello/@@foo comes in, a similar thing happens.

When a request for /hello/@@listDirectory comes in, the framework calls myRootFactory(request), and gets back the root resource. It calls MyRootResource's __getitem__('hello'), and gets back a MyHelloResource instance. It does not call MyHelloResource's __getitem__('listDirectory') (due to the @@ at the lead of listDirectory). Instead, 'listDirectory' becomes the view name and traversal ends. The view name 'listDirectory' is mapped to myapp.views.listRootDirectory, because the context (the last resource traversed) is an instance of MyHelloResource.

When a request for /hello/xyz/@@listDirectory comes in, the framework calls myRootFactory(request), and gets back an instance of MyRootResource. It calls MyRootResource's __getitem__('hello'), and gets back a MyHelloResource instance. It calls MyHelloResource's __getitem__('xyz'), and gets back another MySubdirResource instance. It does not call __getitem__('listDirectory') on the MySubdirResource instance. 'listDirectory' becomes the view name and traversal ends. The view name 'listDirectory' is mapped to myapp.views.listSubDirectory, because the context (the final traversed resource object) is not an instance of MyHelloResource. The view can access the MySubdirResource via request.context.

At we see, traversal is more complicated than URL dispatch. What's the benefit? Well, consider the URL /hello/xyz/abc/listDirectory. This is handled by the above traversal code, but the above URL dispatch code would have to be modified to describe another layer of subdirectories. That is, traversal can handle arbitrarily deep, dynamic hierarchies in a general way, and URL dispatch can't.

You can, if you want to, combine URL dispatch and traversal (in that order). So, we could rewrite the above as:

 1class MyResource(dict):
 2    def __init__(self, name, parent):
 3        self.__name__ = name
 4        self.__parent__ = parent
 5
 6    # returns a MyResource object unconditionally
 7    def __getitem__(self, key):
 8        return MyResource(key, self)
 9
10def myRootFactory(request):
11    return MyResource('', None)
12
13config = Configurator()
14
15config.add_route('helloLogin', '/hello/login')
16config.add_route('helloFoo', '/hello/foo')
17config.add_route('helloList', '/hello/listDirectory')
18config.add_route('list', '/hello/*traverse', factory=myRootFactory)
19
20config.add_view('myapp.views.login', route_name='helloLogin')
21config.add_view('myapp.views.foo', route_name='helloFoo')
22config.add_view('myapp.views.listHelloDirectory', route_name='helloList')
23config.add_view('myapp.views.listSubDirectory', route_name='list',
24                name='listDirectory')

You will be able to visit e.g. http://localhost:8080/hello/foo/bar/@@listDirectory to see the listSubDirectory view.

This is simpler and more readable because we are using URL dispatch to take care of the hardcoded URLs at the top of the tree, and we are using traversal only for the arbitrarily nested subdirectories.

See Also