2: Basic Traversal With Site Roots

Model websites as a hierarchy of objects with operations.

Background

Web applications have URLs which locate data and make operations on that data. Pyramid supports two ways of mapping URLs into Python operations:

  • the more traditional approach of URL dispatch, or routes

  • the more object-oriented approach of traversal popularized by Zope

In this section we will introduce traversal bit-by-bit. Along the way, we will try to show how easy and Pythonic it is to think in terms of traversal.

Traversal is easy, powerful, and useful.

With traversal, you think of your website as a tree of Python objects, just like a dictionary of dictionaries. For example:

http://example.com/company1/aFolder/subFolder/search

...is nothing more than:

>>> root['aFolder']['subFolder'].search()

To remove some mystery about traversal, we start with the smallest possible step: an object at the top of our URL space. This object acts as the "root" and has a view which shows some data on that object.

Objectives

  • Make a factory for the root object.

  • Pass it to the configurator.

  • Have a view which displays an attribute on that object.

Steps

  1. We are going to use the previous step as our starting point:

    $ cd ..; cp -r layout siteroot; cd siteroot
    $ $VENV/bin/python setup.py develop
    
  2. In siteroot/tutorial/__init__.py, make a root factory that points to a function in a module we are about to create:

     1from pyramid.config import Configurator
     2
     3from .resources import bootstrap
     4
     5
     6def main(global_config, **settings):
     7    config = Configurator(settings=settings,
     8                          root_factory=bootstrap)
     9    config.include('pyramid_jinja2')
    10    config.scan('.views')
    11    return config.make_wsgi_app()
    
  3. We add a new file siteroot/tutorial/resources.py with a class for the root of our site, and a factory that returns it:

     1class Root(dict):
     2    __name__ = ''
     3    __parent__ = None
     4    def __init__(self, title):
     5        self.title = title
     6
     7
     8def bootstrap(request):
     9    root = Root('My Site')
    10
    11    return root
    
  4. Our views in siteroot/tutorial/views.py are now very different:

     1from pyramid.view import view_config
     2
     3
     4class TutorialViews:
     5    def __init__(self, context, request):
     6        self.context = context
     7        self.request = request
     8
     9    @view_config(renderer='templates/home.jinja2')
    10    def home(self):
    11        page_title = 'Quick Tutorial: Home'
    12        return dict(page_title=page_title)
    13
    14    @view_config(name='hello', renderer='templates/hello.jinja2')
    15    def hello(self):
    16        page_title = 'Quick Tutorial: Hello'
    17        return dict(page_title=page_title)
    
  5. Rename the template siteroot/tutorial/templates/site.jinja2 to siteroot/tutorial/templates/home.jinja2 and modify it:

    1{% extends "templates/layout.jinja2" %}
    2{% block content %}
    3
    4  <p>Welcome to {{ context.title }}. Visit
    5    <a href="{{ request.resource_url(context, 'hello') }}">hello</a>
    6  </p>
    7
    8{% endblock content %}
    
  6. Add a template in siteroot/tutorial/templates/hello.jinja2:

    1{% extends "templates/layout.jinja2" %}
    2{% block content %}
    3
    4<p>Welcome to {{ context.title }}. Visit 
    5<a href="{{ request.resource_url(context) }}">home</a></p>
    6
    7
    8{% endblock content %}
    
  7. Modify the simple tests in siteroot/tutorial/tests.py:

     1import unittest
     2
     3from pyramid.testing import DummyRequest
     4from pyramid.testing import DummyResource
     5
     6
     7class TutorialViewsUnitTests(unittest.TestCase):
     8    def test_home(self):
     9        from .views import TutorialViews
    10
    11        request = DummyRequest()
    12        title = 'Dummy Context'
    13        context = DummyResource(title=title)
    14        inst = TutorialViews(context, request)
    15        result = inst.home()
    16        self.assertIn('Home', result['page_title'])
    17
    18
    19class TutorialFunctionalTests(unittest.TestCase):
    20    def setUp(self):
    21        from tutorial import main
    22        app = main({})
    23        from webtest import TestApp
    24        self.testapp = TestApp(app)
    25
    26    def test_hello(self):
    27        result = self.testapp.get('/hello', status=200)
    28        self.assertIn(b'Quick Tutorial: Hello', result.body)
    
  8. Now run the tests:

    1$ $VENV/bin/nosetests tutorial
    2..
    3----------------------------------------------------------------------
    4Ran 2 tests in 0.134s
    5
    6OK
    
  9. Run your Pyramid application with:

    $ $VENV/bin/pserve development.ini --reload
    
  10. Open http://localhost:6543/hello in your browser.

Analysis

Our __init__.py has a small but important change: we create the configuration with a root factory. Our root factory is a simple function that performs some work and returns the root object in the resource tree.

In the resource tree, Pyramid can match URLs to objects and subobjects, finishing in a view as the operation to perform. Traversing through containers is done using Python's normal __getitem__ dictionary protocol.

Pyramid provides services beyond simple Python dictionaries. These location services need a little bit more protocol than just __getitem__. Namely, objects need to provide an attribute/callable for __name__ and __parent__.

In this step, our tree has one object: the root. It is an instance of our Root class. The next URL hop is hello. Our root instance does not have an item in its dictionary named hello, so Pyramid looks for a view with a name=hello, finding our view method.

Our home view is passed by Pyramid, with the instance of this folder as context. The view can then grab attributes and other data from the object that is the focus of the URL.

Now on to the most visible part: no more routes! Previously we wrote URL "replacement patterns" which mapped to a route. The route extracted data from the patterns and made this data available to views that were mapped to that route.

Instead segments in URLs become object identifiers in Python.

Extra Credit

  1. Is the root factory called once on startup, or on every request? Do a small change that answers this. What is the impact of the answer on this?