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¶
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
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()
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
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)
Rename the template
siteroot/tutorial/templates/site.jinja2
tositeroot/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 %}
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 %}
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)
Now run the tests:
1$ $VENV/bin/nosetests tutorial 2.. 3---------------------------------------------------------------------- 4Ran 2 tests in 0.134s 5 6OK
Run your Pyramid application with:
$ $VENV/bin/pserve development.ini --reload
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¶
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?