diff options
| author | Chris McDonough <chrism@agendaless.com> | 2008-07-20 19:13:42 +0000 |
|---|---|---|
| committer | Chris McDonough <chrism@agendaless.com> | 2008-07-20 19:13:42 +0000 |
| commit | 0bc787d4999460d4219c621f83c619ca7c4552c2 (patch) | |
| tree | 1f6c47221ffede2b68ef6c288c61ae6027a1cfa1 /docs | |
| parent | b54cdb6d0951a28b7d7bf4f585a4059cc5e6b18a (diff) | |
| download | pyramid-0bc787d4999460d4219c621f83c619ca7c4552c2.tar.gz pyramid-0bc787d4999460d4219c621f83c619ca7c4552c2.tar.bz2 pyramid-0bc787d4999460d4219c621f83c619ca7c4552c2.zip | |
More docs; fix autogen app model root creation.
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/index.rst | 1 | ||||
| -rw-r--r-- | docs/narr/introduction.rst | 457 | ||||
| -rw-r--r-- | docs/narr/project.rst | 199 | ||||
| -rw-r--r-- | docs/narr/traversal.rst | 269 |
4 files changed, 460 insertions, 466 deletions
diff --git a/docs/index.rst b/docs/index.rst index 28b2d91d4..f7bf25bd0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,6 +14,7 @@ Narrative documentation in chapter form explaining how to use narr/introduction narr/project + narr/traversal narr/views narr/templates narr/models diff --git a/docs/narr/introduction.rst b/docs/narr/introduction.rst index 0782af2fb..438e4008b 100644 --- a/docs/narr/introduction.rst +++ b/docs/narr/introduction.rst @@ -1,83 +1,34 @@ repoze.bfg Introduction ======================= -``repoze.bfg`` is a system for routing web requests to applications -based on graph traversal. It is inspired by Zope's publisher, and -uses Zope libraries to do much of its work. However, it is less -ambitious and less featureful than any released version of Zope's -publisher. +``repoze.bfg`` is a web application framework based on graph +traversal. It is inspired by Zope's publisher, and uses Zope +libraries to do much of its work. However, it is less ambitious and +less featureful than any released version of Zope's publisher. ``repoze.bfg`` uses the WSGI protocol to handle requests and responses, and integrates Zope, Paste, and WebOb libraries to form the basis for a simple web object publishing framework. -Graph Traversal ---------------- - -In many popular web frameworks, a "URL dispatcher" is used to -associate a particular URL with a bit of code (known somewhat -ambiguously as a "controller" or "view" depending upon the particular -vocabulary religion to which you subscribe). These systems allow the -developer to create "urlconfs" or "routes" to controller/view Python -code using pattern matching against URL components. Examples: -`Django's URL dispatcher -<http://www.djangoproject.com/documentation/url_dispatch/>`_ and the -`Routes URL mapping system <http://routes.groovie.org/>`_ . - -It is however possible to map URLs to code differently, using object -graph traversal. The venerable Zope and CherryPy web frameworks offer -graph-traversal-based URL dispatch. ``repoze.bfg`` also provides -graph-traversal-based dispatch of URLs to code. Graph-traversal based -dispatching is useful if you like the URL to be representative of an -arbitrary hierarchy of potentially heterogeneous items. - -Non-graph traversal based URL dispatch can easily handle URLs such as -``http://example.com/members/Chris``, where it's assumed that each -item "below" ``members`` in the URL represents a member in the system. -You just match everything "below" ``members`` to a particular view. -They are not very good, however, at inferring the difference between -sets of URLs such as ``http://example.com/members/Chris/document`` vs. -``http://example.com/members/Chris/stuff/page`` wherein you'd like the -``document`` in the first URL to represent, e.g. a PDF document, and -``/stuff/page`` in the second to represent, e.g. an OpenOffice -document in a "stuff" folder. It takes more pattern matching -assertions to be able to make URLs like these work in URL-dispatch -based systems, and some assertions just aren't possible. For example, -URL-dispatch based systems don't deal very well with URLs that -represent arbitrary-depth hierarchies. - -Graph traversal works well if you need to divine meaning out of these -types of "ambiguous" URLs and URLs that represent arbitrary-depth -hierarchies. Each URL segment represents a single traversal through -an edge of the graph. So a URL like ``http://example.com/a/b/c`` can -be thought of as a graph traversal on the example.com site through the -edges "a", "b", and "c". - -Finally, if you're willing to treat your application models as a graph -that can be traversed, it also becomes trivial to provide "row-level -security" (in common relational parlance): you just attach a security -declaration to each instance in the graph. This is not as easy in -frameworks that use URL-based dispatch. - -Graph traversal is materially more complex than URL-based dispatch, -however, if only because it requires the construction and maintenance -of a graph, and it requires the developer to think about mapping URLs -to code in terms of traversing the graph. (How's *that* for -self-referential! ;-) That said, for developers comfortable with Zope, -in particular, and comfortable with hierarchical data stores like -ZODB, mapping a URL to a graph traversal it's a natural way to think -about creating a web application. - -In essence, the choice to use graph traversal vs. URL dispatch is -largely religious in some sense. Graph traversal dispatch probably -just doesn't make any sense when you possess completely "square" data -stored in a relational database. However, when you have a -hierarchical data store, it can provide advantages over using -URL-based dispatch. - Similarities with Other Frameworks ---------------------------------- +``repoze.bfg`` was inspired by Zope, Django, and Pylons. + +``repoze.bfg``'s traversal is inspired by Zope. ``repoze.bfg`` uses +the Zope Component Architecture ("CA") internally like Zope 2, Zope3, +and Grok. Developers don't interact with the CA very much during +typical development, however; it's mostly used by the framework +developer rather than the application developer. + +Like Pylons, ``repoze.bfg`` is mostly policy-free. It makes no +assertions about which database you should use, and its built-in +templating facilities are only for convenience. It is essentially +only supplies a mechanism to map URLs to view code and convention for +calling those views. You are free to use third-party components in +your application that fit your needs. Also like Pylons, +``repoze.bfg`` is heavily dependent on WSGI. + The Django docs state that Django is an "MTV" framework in their `FAQ <http://www.djangoproject.com/documentation/faq/>`_. This also happens to be true for ``repoze.bfg``:: @@ -110,23 +61,6 @@ happens to be true for ``repoze.bfg``:: framework - that is, "model", "template", and "view." That breakdown makes much more sense. -How ``repoze.bfg`` is Configured --------------------------------- - -Users interact with your ``repoze.bfg``-based application via a -"router", which is itself a WSGI application. At system startup time, -the router must be configured with a root object from which all -traversal will begin. The root object is a mapping object, such as a -Python dictionary. In fact, all items contained in the graph are -either leaf nodes (these have no __getitem__) or container nodes -(these do have a __getitem__). - -Items contained within the graph are analogous to the concept of -``model`` objects used by many other frameworks. They are typically -instances of classes. Each containerish instance is willing to return -a child or raise a KeyError based on a name passed to its __getitem__. -No leaf-level instance is required to have a __getitem__. - Jargon ------ @@ -163,12 +97,13 @@ model context - A model in the system that is the subject of a view. + A model in the system that is found during traversal; it becomes the + subject of a view. -view registry (aka application registry) +application registry - A registry which maps a context and view name to a view callable and - optionally a permission. + A registry which maps model types to views, as well as performing + other application-specific component registrations. template @@ -201,345 +136,3 @@ permission to which principal to answer the question "is this user allowed to do this". -How ``repoze.bfg`` Processes a Request --------------------------------------- - -When a user requests a page from your ``repoze.bfg`` -powered -application, the system uses this algorithm to determine which Python -code to execute: - - 1. The request for the page is presented to ``repoze.bfg``'s - "router" in terms of a standard WSGI request, which is - represented by a WSGI environment and a start_response callable. - - 2. The router creates a `WebOb <http://pythonpaste.org/webob/>`_ - request object based on the WSGI environment. - - 3. The router uses the WSGI environment's ``PATH_INFO`` variable to - determine the path segments to traverse. The leading slash is - stripped off `PATH_INFO``, and the remaining path segments are - split on the slash character to form a traversal sequence, so a - request with a ``PATH_INFO`` variable of ``/a/b/c`` maps to the - traversal sequence ``['a', 'b', 'c']``. - - 4. Traversal begins at the root object. For the traversal sequence - ``['a', 'b', 'c']``, the root object's __getitem__ is called with - the name ``a``. Traversal continues through the sequence. In - our example, if the root object's __getitem__ called with the - name ``a`` returns an object (aka "object A"), that object's - __getitem__ is called with the name ``b``. If object A returns - an object when asked for ``b``, object B's __getitem__ is then - asked for the name ``c``, and may return object C. - - 5. Traversal ends when a) the entire path is exhausted or b) when - any graph element raises a KeyError from its __getitem__ or c) - when any non-final path element traversal does not have a - __getitem__ method (resulting in a NameError) or d) when any path - element is prefixed with the set of characters ``@@`` (indicating - that the characters following the ``@@`` token should be treated - as a "view name"). - - 6. When traversal ends for any of the reasons in the previous step, - the the last object found during traversal is deemed to be the - "context". If the path has been exhausted when traversal ends, - the "view name" is deemed to be the empty string (``''``). - However, if the path was not exhausted before traversal - terminated, the first remaining path element is treated as the - view name. Any subseqent path elements after the view name are - deemed the "subpath". For instance, if ``PATH_INFO`` was - ``/a/b`` and the root returned an "A" object, and the "A" object - returned a "B" object, the router deems that the context is - "object B", the view name is the empty string, and the subpath is - the empty sequence. On the other hand, if ``PATH_INFO`` was - ``/a/b/c`` and "object A" was found but raised a KeyError for the - name ``b``, the router deems that the context is object A, the - view name is ``b`` and the subpath is ``['c']``. - - 7. If a security policy is configured, the router performs a - permission lookup. If a permission declaration is found for the - view name and context implied by the current request, the - security policy is consulted to see if the "current user" (also - determined by the security policy) can perform the action. If he - can, processing continues. If he cannot, an HTTPUnauthorized - error is raised. - - 8. Armed with the context, the view name, and the subpath, the - router performs a view lookup. It attemtps to look up a view - from the ``repoze.bfg`` view registry using the view name and the - context. If a view factory is found, it is called with the - context and the request. It returns a response, which is fed - back upstream. If a view is not found, a generic WSGI - ``NotFound`` application is constructed. - -In either case, the result is returned upstream via the WSGI protocol. - -A Traversal Example -------------------- - -Let's pretend the user asks for -``http://example.com/foo/bar/baz/biz/buz.txt``. Let's pretend that the -request's ``PATH_INFO`` in that case is ``/foo/bar/baz/biz/buz.txt``. -Let's further pretend that when this request comes in that we're -traversing the follwing graph:: - - /-- - | - |-- foo - | - ----bar - -Here's what happens: - - - bfg traverses the root, and attempts to find foo, which it finds. - - - bfg traverses foo, and attempts to find bar, which it finds. - - - bfg traverses bar, and attempts to find baz, which it does not - find ('bar' raises a ``KeyError`` when asked for baz). - -The fact that it does not find "baz" at this point does not signify an -error condition. It signifies that: - - - the "context" is bar (the context is the last item found during - traversal). - - - the "view name" is ``baz`` - - - the "subpath" is ``['biz', 'buz.txt']`` - -Because it's the "context", bfg examimes "baz" to find out what "type" -it is. Let's say it finds that the context an ``IBar`` type (because -"bar" happens to have an attribute attached to it that indicates it's -an ``IBar``). - -Using the "view name" ("baz") and the type, it asks the "view -registry" (configured separately, in our case via "configure.zcml") -this question: - - - Please find me a "view" (controller in some religions) with the - name "baz" that can be used for the type ``IBar``. - -Let's say it finds no matching view type. It then returns a NotFound. -The request ends. Everyone is sad. - -But! For this graph:: - - /-- - | - |-- foo - | - ----bar - | - ----baz - | - biz - -The user asks for ``http://example.com/foo/bar/baz/biz/buz.txt`` - - - bfg traverses foo, and attempts to find bar, which it finds. - - - bfg traverses bar, and attempts to find baz, which it finds. - - - bfg traverses baz, and attempts to find biz, which it finds. - - - bfg traverses biz, and attemtps to find "buz.txt" which it does - not find. - -The fact that it does not find "biz.txt" at this point does not -signify an error condition. It signifies that: - - - the "context" is biz (the context is the last item found during traversal). - - - the "view name" is "buz.txt" - - - the "subpath" is the empty list [] - -Because it's the "context", bfg examimes "biz" to find out what "type" -it is. Let's say it finds that the context an ``IBiz`` type (because -"biz" happens to have an attribute attached to it that happens -indicates it's an ``IBiz``). - -Using the "view name" ("buz.txt") and the type, it asks the "view -registry" (configured separately, in our case in "configure.zcml") -this question: - - - Please find me a "view" (controller in some religions) with the name - "buz.txt" that can be used for type ``IBiz``. - -Let's say that question is answered "here you go, here'a a bit of code -that is willing to deal with that case", and returns a view. It is -passed the "biz" object as the "context" and the current WebOb request -as the "request". It returns a response. - -There are two special cases: - -- During traversal you will often end up with a "view name" that is - the empty string. This indicates that ``repoze.bfg`` should look up - the *default view*. The default view is a view that is registered - with no name or a view which is registered with a name that equals - the empty string. - -- If any path segment element begins with the special characters - ``@@`` (think of them as goggles), that element is considered the - view name immediately and traversal stops there. This allows you to - address views that may have the same names as model instance names - without conflict. - -A Sample Application --------------------- - -A typical simple ``repoze.bfg`` application consists of four things: - - 1. A ``views.py`` module, which contains view code. - - 2. A ``models.py`` module, which contains model code. - - 3. A ``configure.zcml`` file which maps view names to model types. - This is also known as the "view registry", although it also - often contains non-view-related declarations. - - 4. A "templates" directory, which is full of zc3.pt templates. - -An application must be a Python package (meaning it must have an -__init__.py and it must be findable on the PYTHONPATH). - -We don't describe any security in our very simple sample application. -Security is optional in a repoze.bfg application; it needn't be used -until necessary. - -views.py -~~~~~~~~ - -A views.py module might look like so:: - - from webob import Response - from repoze.bfg.template import render_template_to_response - - def my_hello_view(context, request): - response = Response('Hello from %s @ %s' % ( - context.__name__, - request.environ['PATH_INFO'])) - return response - - def my_template_view(context, request): - return render_template_to_response('templates/my.pt', name=context.__name__) - -models.py -~~~~~~~~~ - -A models.py might look like so:: - - from UserDict import UserDict - - from zope.interface import implements - from zope.interface import Attribute - from zope.interface import Interface - - class IMyModel(Interface): - __name__ = Attribute('Name of the model instance') - - class MyModel(UserDict): - implements(IMyModel) - def __init__(self, name): - self.__name__ = name - UserDict.__init__(self, {}) - - # model instance info would typically be stored in a database of some - # kind; here we put it at module scope for demonstration purposes. - - root = MyModel('root') - root['a'] = MyModel('a') - root['b'] = MyModel('b') - - def get_root(environ): - return root - -configure.zcml -~~~~~~~~~~~~~~ - -A view registry might look like so:: - - <configure xmlns="http://namespaces.zope.org/zope" - xmlns:bfg="http://namespaces.repoze.org/bfg" - i18n_domain="repoze.bfg"> - - <!-- this must be included for the view declarations to work --> - <include package="repoze.bfg" /> - - <!-- the default view for a MyModel --> - <bfg:view - for=".models.IMyModel" - view=".views.my_hello_view" - permission="read" - /> - - <!-- the templated.html view for a MyModel --> - <bfg:view - for=".models.IMyModel" - view=".views.my_template_view" - name="templated.html" - permission="read" - /> - - </configure> - -templates/my.pt -~~~~~~~~~~~~~~~ - -A template that is used by a view might look like so:: - - <html xmlns="http://www.w3.org/1999/xhtml" - xmlns:tal="http://xml.zope.org/namespaces/tal"> - <head></head> - <body> - <h1>My template viewing ${name}</h1> - </body> - </html> - -Running the Application ------------------------ - -To run the application above, the simplest method is to run it -directly from a starter script (although you might also use Paste to -perform this task):: - - from paste import httpserver - - from repoze.bfg import make_app - from myapp.models import get_root - import myapp - - app = make_app(get_root, myapp) - httpserver.serve(app, host='0.0.0.0', port='5432') - -Viewing the Application ------------------------ - -Visit http://localhost:5432/ in your browser. You will see:: - - Hello from root @ / - -Visit http://localhost:5432/a in your browser. You will see:: - - Hello from a @ /a - -Visit http://localhost:5432/b in your browser. You will see:: - - Hello from b @ /b - -Visit http://localhost:5432/templated.html in your browser. You will -see:: - - My template viewing root - - -Visit http://localhost:5432/a/templated.html in your browser. You -will see:: - - My template viewing a - -Visit http://localhost:5432/b/templated.html in your browser. You -will see:: - - My template viewing b - diff --git a/docs/narr/project.rst b/docs/narr/project.rst index 7c48874b7..c12e0db71 100644 --- a/docs/narr/project.rst +++ b/docs/narr/project.rst @@ -1,6 +1,12 @@ Starting a ``repoze.bfg`` Project ================================= +You can use ``repoze.bfg`` 's sample application generator to get +started. + +Creating the Project +-------------------- + To start a ``repoze.bfg`` project, use the ``paster create`` facility:: @@ -33,12 +39,12 @@ facility:: Running /Users/chrism/projects/repoze-devel/bfg/bin/python setup.py egg_info The project will be created in a directory named ``myproject``. That -directory is a directory from which a Python setuptools *distribution* -can be created. THe ``setup.py`` file in that directory can be used -to distribute your application, or install your application for -deployment or development. A sample PasteDeploy ``.ini`` file named -``myproject.ini`` will also be created in the project directory. You -can use this to run your application. +directory is a setuptools *project* directory from which a Python +setuptools *distribution* can be created. The ``setup.py`` file in +that directory can be used to distribute your application, or install +your application for deployment or development. A sample PasteDeploy +``.ini`` file named ``myproject.ini`` will also be created in the +project directory. You can use this to run your application. The main ``myproject`` contains an additional subdirectory (also named ``myproject``) representing a Python pakckage which holds very simple @@ -48,9 +54,11 @@ code and templates. Installing your Newly Created Project for Development ----------------------------------------------------- -Using your favorite Python interpreter (using a virtualenv suggested -in order to get isolation), invoke the following command when inside -the project directory against the generated ``setup.py``:: +Using your favorite Python interpreter (using a `virtualenv +<http://pypi.python.org/pypi/virtualenv>`_ is suggested in order to +isolate your application from your system Python's packages), invoke +the following command when inside the project directory against the +generated ``setup.py``:: $ python setup.py develop ... @@ -59,29 +67,6 @@ the project directory against the generated ``setup.py``:: This will install your application 's package into the interpreter so it can be found and run under a webserver. -Runnning The Project Application --------------------------------- - -Once the project is installed for development, you can run it using -the ``paster serve`` command against the generated ``myproject.ini`` -configuration file:: - - $ paster serve myproject/myproject.ini - Starting server in PID 16601. - serving on 0.0.0.0:5432 view at http://127.0.0.1:5432 - -It will listen on port 5432. If you visit the unchanged sample -application using a browser (e.g. http://localhost:5432/), you will -see the following:: - - Welcome to myproject - -.. note:: During development, it's often useful to run ``paster serve`` - using its ``--reload`` option. When any Python module your project - uses, changes, it will restart the server, which makes development - easier, as changes to Python code under ``repoze.bfg`` is not put - into effect until the server restarts. - Running The Tests For Your Application -------------------------------------- @@ -104,5 +89,151 @@ To run unit tests for your application, you should invoke them like so:: OK -The tests are found in the ``tests.py`` module in your generated -project. One sample test exists. +The tests are found in the ``tests.py`` module in your +paster-create-generated project. One sample test exists. + +Runnning The Project Application +-------------------------------- + +Once the project is installed for development, you can run the +application it represents using the ``paster serve`` command against +the generated ``myproject.ini`` configuration file:: + + $ paster serve myproject/myproject.ini + Starting server in PID 16601. + serving on 0.0.0.0:5432 view at http://127.0.0.1:5432 + +It will listen on port 5432. + +.. note:: During development, it's often useful to run ``paster serve`` + using its ``--reload`` option. When any Python module your project + uses, changes, it will restart the server, which makes development + easier, as changes to Python code under ``repoze.bfg`` is not put + into effect until the server restarts. + +Viewing the Application +----------------------- + +Visit http://localhost:5432/ in your browser. You will see:: + + Welcome to myproject + +The Project Structure +--------------------- + +Our generated ``repoze.bfg`` application is a setuptools *project* +(named ``myproject``), which contains a Python package (also named +``myproject``). + +The ``myproject`` package has the following files and directories: + + 1. A ``views.py`` module, which contains view code. + + 2. A ``models.py`` module, which contains model code. + + 3. A ``run.py`` module, which contains code that helps users run the + application. + + 4. A ``configure.zcml`` file which maps view names to model types. + This is also known as the "application registry", although it + also often contains non-view-related declarations. + + 5. A ``templates`` directory, which is full of zc3.pt and/or XSL + templates. + +This is purely by convention: ``repoze.bfg`` doesn't insist that you +name things in any particular way. + +We don't describe any security in our sample application. Security is +optional in a repoze.bfg application; it needn't be used until +necessary. + +``views.py`` +------------ + +The code in the views.py project looks like this:: + + from repoze.bfg.template import render_template_to_response + + def my_view(context, request): + return render_template_to_response('templates/mytemplate.pt', + project = 'myproject') + +``models.py`` +------------- + +The code in the models.py looks like this:: + + from zope.interface import Interface + from zope.interface import implements + + class IMyModel(Interface): + pass + + class MyModel(object): + implements(IMyModel) + pass + + root = MyModel() + + def get_root(environ): + return root + +In a "real" application, the root object would not be such a simple +object. Instead, it would be an object that could access some +persistent data store, such as a database. ``repoze.bfg`` doesn't +make any assumption about which sort of datastore you'll want to use, +so the sample application uses an instance of ``MyModel`` to represent +the root. + +``configure.zcml`` +------------------ + +The ``configure.zcml`` (representing the application registry) looks +like so:: + + <configure xmlns="http://namespaces.zope.org/zope" + xmlns:bfg="http://namespaces.repoze.org/bfg" + i18n_domain="repoze.bfg"> + + <!-- this must be included for the view declarations to work --> + <include package="repoze.bfg" /> + + <bfg:view + for=".models.IMyModel" + view=".views.my_view" + /> + + </configure> + +``templates/my.pt`` +------------------- + +The single template in the project looks like so:: + + <html xmlns="http://www.w3.org/1999/xhtml" + xmlns:tal="http://xml.zope.org/namespaces/tal"> + <head></head> + <body> + <h1>Welcome to ${project}</h1> + </body> + </html> + +``run.py`` +---------- + +The run.py file looks like so:: + + def make_app(global_config, **kw): + # paster app config callback + from repoze.bfg import make_app + from myproject.models import get_root + import myproject + app = make_app(get_root, myproject) + return app + + if __name__ == '__main__': + from paste import httpserver + app = make_app(None) + httpserver.serve(app, host='0.0.0.0', port='5432') + diff --git a/docs/narr/traversal.rst b/docs/narr/traversal.rst new file mode 100644 index 000000000..3e7c6125b --- /dev/null +++ b/docs/narr/traversal.rst @@ -0,0 +1,269 @@ +Traversal +========= + +In many popular web frameworks, a "URL dispatcher" is used to +associate a particular URL with a bit of code (known somewhat +ambiguously as a "controller" or "view" depending upon the particular +vocabulary religion to which you subscribe). These systems allow the +developer to create "urlconfs" or "routes" to controller/view Python +code using pattern matching against URL components. Examples: +`Django's URL dispatcher +<http://www.djangoproject.com/documentation/url_dispatch/>`_ and the +`Routes URL mapping system <http://routes.groovie.org/>`_ . + +It is however possible to map URLs to code differently, using object +graph traversal. The venerable Zope and CherryPy web frameworks offer +graph-traversal-based URL dispatch. ``repoze.bfg`` also provides +graph-traversal-based dispatch of URLs to code. Graph-traversal based +dispatching is useful if you like the URL to be representative of an +arbitrary hierarchy of potentially heterogeneous items. + +Non-graph traversal based URL dispatch can easily handle URLs such as +``http://example.com/members/Chris``, where it's assumed that each +item "below" ``members`` in the URL represents a member in the system. +You just match everything "below" ``members`` to a particular view. +They are not very good, however, at inferring the difference between +sets of URLs such as ``http://example.com/members/Chris/document`` vs. +``http://example.com/members/Chris/stuff/page`` wherein you'd like the +``document`` in the first URL to represent, e.g. a PDF document, and +``/stuff/page`` in the second to represent, e.g. an OpenOffice +document in a "stuff" folder. It takes more pattern matching +assertions to be able to make URLs like these work in URL-dispatch +based systems, and some assertions just aren't possible. For example, +URL-dispatch based systems don't deal very well with URLs that +represent arbitrary-depth hierarchies. + +Graph traversal works well if you need to divine meaning out of these +types of "ambiguous" URLs and URLs that represent arbitrary-depth +hierarchies. Each URL segment represents a single traversal through +an edge of the graph. So a URL like ``http://example.com/a/b/c`` can +be thought of as a graph traversal on the example.com site through the +edges "a", "b", and "c". + +Finally, if you're willing to treat your application models as a graph +that can be traversed, it also becomes trivial to provide "row-level +security" (in common relational parlance): you just attach a security +declaration to each instance in the graph. This is not as easy in +frameworks that use URL-based dispatch. + +Graph traversal is materially more complex than URL-based dispatch, +however, if only because it requires the construction and maintenance +of a graph, and it requires the developer to think about mapping URLs +to code in terms of traversing the graph. (How's *that* for +self-referential! ;-) That said, for developers comfortable with Zope, +in particular, and comfortable with hierarchical data stores like +ZODB, mapping a URL to a graph traversal it's a natural way to think +about creating a web application. + +In essence, the choice to use graph traversal vs. URL dispatch is +largely religious in some sense. Graph traversal dispatch probably +just doesn't make any sense when you possess completely "square" data +stored in a relational database. However, when you have a +hierarchical data store, it can provide advantages over using +URL-based dispatch. + +The Model Graph +--------------- + +Users interact with your ``repoze.bfg``-based application via a +"router", which is itself a WSGI application. At system startup time, +the router is configured with a root object from which all traversal +will begin. The root object is a mapping object, such as a Python +dictionary. In fact, all items contained in the graph are either leaf +nodes (these have no ``__getitem__``) or container nodes (these do +have a ``__getitem__``). + +Items contained within the graph are analogous to the concept of +``model`` objects used by many other frameworks (and ``repoze.bfg`` +refers to them as models, as well). They are typically instances of +classes. Each containerish instance is willing to return a child or +raise a KeyError based on a name passed to its ``__getitem__``. No +leaf-level instance is required to have a ``__getitem__``. + +``repoze.bfg`` traverses the model graph in order to find a *context*. +It then attempts to find a *view* based on the type of the context. + +How ``repoze.bfg`` Processes a Request Using Traversal +------------------------------------------------------ + +When a user requests a page from your ``repoze.bfg`` -powered +application, the system uses this algorithm to determine which Python +code to execute: + + 1. The request for the page is presented to ``repoze.bfg``'s + "router" in terms of a standard WSGI request, which is + represented by a WSGI environment and a start_response callable. + + 2. The router creates a `WebOb <http://pythonpaste.org/webob/>`_ + request object based on the WSGI environment. + + 3. The router uses the WSGI environment's ``PATH_INFO`` variable to + determine the path segments to traverse. The leading slash is + stripped off ``PATH_INFO``, and the remaining path segments are + split on the slash character to form a traversal sequence, so a + request with a ``PATH_INFO`` variable of ``/a/b/c`` maps to the + traversal sequence ``['a', 'b', 'c']``. + + 4. Traversal begins at the root object. For the traversal sequence + ``['a', 'b', 'c']``, the root object's ``__getitem__`` is called + with the name ``a``. Traversal continues through the sequence. + In our example, if the root object's ``__getitem__`` called with + the name ``a`` returns an object (aka "object A"), that object's + ``__getitem__`` is called with the name ``b``. If object A + returns an object when asked for ``b``, object B's + ``__getitem__`` is then asked for the name ``c``, and may return + object C. + + 5. Traversal ends when a) the entire path is exhausted or b) when + any graph element raises a KeyError from its ``__getitem__`` or + c) when any non-final path element traversal does not have a + ``__getitem__`` method (resulting in a NameError) or d) when any + path element is prefixed with the set of characters ``@@`` + (indicating that the characters following the ``@@`` token should + be treated as a "view name"). + + 6. When traversal ends for any of the reasons in the previous step, + the the last object found during traversal is deemed to be the + "context". If the path has been exhausted when traversal ends, + the "view name" is deemed to be the empty string (``''``). + However, if the path was not exhausted before traversal + terminated, the first remaining path element is treated as the + view name. Any subseqent path elements after the view name are + deemed the "subpath". For instance, if ``PATH_INFO`` was + ``/a/b`` and the root returned an "A" object, and the "A" object + returned a "B" object, the router deems that the context is + "object B", the view name is the empty string, and the subpath is + the empty sequence. On the other hand, if ``PATH_INFO`` was + ``/a/b/c`` and "object A" was found but raised a KeyError for the + name ``b``, the router deems that the context is object A, the + view name is ``b`` and the subpath is ``['c']``. + + 7. If a security policy is configured, the router performs a + permission lookup. If a permission declaration is found for the + view name and context implied by the current request, the + security policy is consulted to see if the "current user" (also + determined by the security policy) can perform the action. If he + can, processing continues. If he cannot, an HTTPUnauthorized + error is raised. + + 8. Armed with the context, the view name, and the subpath, the + router performs a view lookup. It attemtps to look up a view + from the ``repoze.bfg`` application registry using the view name + and the context. If a view factory is found, it is called with + the context and the request. It returns a response, which is fed + back upstream. If a view is not found, a generic WSGI + ``NotFound`` application is constructed. + +In either case, the result is returned upstream via the WSGI protocol. + +A Traversal Example +------------------- + +Let's pretend the user asks for +``http://example.com/foo/bar/baz/biz/buz.txt``. Let's pretend that the +request's ``PATH_INFO`` in that case is ``/foo/bar/baz/biz/buz.txt``. +Let's further pretend that when this request comes in that we're +traversing the follwing graph:: + + /-- + | + |-- foo + | + ----bar + +Here's what happens: + + - bfg traverses the root, and attempts to find foo, which it finds. + + - bfg traverses foo, and attempts to find bar, which it finds. + + - bfg traverses bar, and attempts to find baz, which it does not + find ('bar' raises a ``KeyError`` when asked for baz). + +The fact that it does not find "baz" at this point does not signify an +error condition. It signifies that: + + - the "context" is bar (the context is the last item found during + traversal). + + - the "view name" is ``baz`` + + - the "subpath" is ``['biz', 'buz.txt']`` + +Because it's the "context", bfg examimes "baz" to find out what "type" +it is. Let's say it finds that the context an ``IBar`` type (because +"bar" happens to have an attribute attached to it that indicates it's +an ``IBar``). + +Using the "view name" ("baz") and the type, it asks the "application +registry" (configured separately, via "configure.zcml") this question: + + - Please find me a "view" (controller in some religions) with the + name "baz" that can be used for the type ``IBar``. + +Let's say it finds no matching view type. It then returns a NotFound. +The request ends. Everyone is sad. + +But! For this graph:: + + /-- + | + |-- foo + | + ----bar + | + ----baz + | + biz + +The user asks for ``http://example.com/foo/bar/baz/biz/buz.txt`` + + - bfg traverses foo, and attempts to find bar, which it finds. + + - bfg traverses bar, and attempts to find baz, which it finds. + + - bfg traverses baz, and attempts to find biz, which it finds. + + - bfg traverses biz, and attemtps to find "buz.txt" which it does + not find. + +The fact that it does not find "biz.txt" at this point does not +signify an error condition. It signifies that: + + - the "context" is biz (the context is the last item found during traversal). + + - the "view name" is "buz.txt" + + - the "subpath" is the empty list [] + +Because it's the "context", bfg examimes "biz" to find out what "type" +it is. Let's say it finds that the context an ``IBiz`` type (because +"biz" happens to have an attribute attached to it that happens +indicates it's an ``IBiz``). + +Using the "view name" ("buz.txt") and the type, it asks the +"application registry" (configured separately, in "configure.zcml") +this question: + + - Please find me a "view" (controller in some religions) with the name + "buz.txt" that can be used for type ``IBiz``. + +Let's say that question is answered "here you go, here'a a bit of code +that is willing to deal with that case", and returns a view. It is +passed the "biz" object as the "context" and the current WebOb request +as the "request". It returns a response. + +There are two special cases: + +- During traversal you will often end up with a "view name" that is + the empty string. This indicates that ``repoze.bfg`` should look up + the *default view*. The default view is a view that is registered + with no name or a view which is registered with a name that equals + the empty string. + +- If any path segment element begins with the special characters + ``@@`` (think of them as goggles), that segment is considered the + "view name" immediately and traversal stops there. This allows you + to address views that may have the same names as model instance + names in the graph umambiguously. + |
