[Checkins] SVN: traject/ Initial import.

Martijn Faassen faassen at startifact.com
Fri Oct 23 11:27:49 EDT 2009


Log message for revision 105243:
  Initial import.
  

Changed:
  A   traject/
  A   traject/trunk/
  A   traject/trunk/bootstrap.py
  A   traject/trunk/buildout.cfg
  A   traject/trunk/scripts/
  A   traject/trunk/scripts/performance.py
  A   traject/trunk/setup.py
  A   traject/trunk/src/
  A   traject/trunk/src/traject/
  A   traject/trunk/src/traject/__init__.py
  A   traject/trunk/src/traject/_traject.py
  A   traject/trunk/src/traject/tests.py
  A   traject/trunk/src/traject/traject.txt

-=-
Added: traject/trunk/bootstrap.py
===================================================================
--- traject/trunk/bootstrap.py	                        (rev 0)
+++ traject/trunk/bootstrap.py	2009-10-23 15:27:49 UTC (rev 105243)
@@ -0,0 +1,84 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Bootstrap a buildout-based project
+
+Simply run this script in a directory containing a buildout.cfg.
+The script accepts buildout command-line options, so you can
+use the -c option to specify an alternate configuration file.
+
+$Id$
+"""
+
+import os, shutil, sys, tempfile, urllib2
+
+tmpeggs = tempfile.mkdtemp()
+
+is_jython = sys.platform.startswith('java')
+
+try:
+    import pkg_resources
+except ImportError:
+    ez = {}
+    exec urllib2.urlopen('http://peak.telecommunity.com/dist/ez_setup.py'
+                         ).read() in ez
+    ez['use_setuptools'](to_dir=tmpeggs, download_delay=0)
+
+    import pkg_resources
+
+if sys.platform == 'win32':
+    def quote(c):
+        if ' ' in c:
+            return '"%s"' % c # work around spawn lamosity on windows
+        else:
+            return c
+else:
+    def quote (c):
+        return c
+
+cmd = 'from setuptools.command.easy_install import main; main()'
+ws  = pkg_resources.working_set
+
+if len(sys.argv) > 2 and sys.argv[1] == '--version':
+    VERSION = '==%s' % sys.argv[2]
+    args = sys.argv[3:] + ['bootstrap']
+else:
+    VERSION = ''
+    args = sys.argv[1:] + ['bootstrap']
+
+if is_jython:
+    import subprocess
+
+    assert subprocess.Popen([sys.executable] + ['-c', quote(cmd), '-mqNxd',
+           quote(tmpeggs), 'zc.buildout' + VERSION],
+           env=dict(os.environ,
+               PYTHONPATH=
+               ws.find(pkg_resources.Requirement.parse('setuptools')).location
+               ),
+           ).wait() == 0
+
+else:
+    assert os.spawnle(
+        os.P_WAIT, sys.executable, quote (sys.executable),
+        '-c', quote (cmd), '-mqNxd', quote (tmpeggs), 'zc.buildout' + VERSION,
+        dict(os.environ,
+            PYTHONPATH=
+            ws.find(pkg_resources.Requirement.parse('setuptools')).location
+            ),
+        ) == 0
+
+ws.add_entry(tmpeggs)
+ws.require('zc.buildout' + VERSION)
+import zc.buildout.buildout
+zc.buildout.buildout.main(args)
+shutil.rmtree(tmpeggs)

Added: traject/trunk/buildout.cfg
===================================================================
--- traject/trunk/buildout.cfg	                        (rev 0)
+++ traject/trunk/buildout.cfg	2009-10-23 15:27:49 UTC (rev 105243)
@@ -0,0 +1,13 @@
+[buildout]
+develop = .
+parts = devpython test
+
+[devpython]
+recipe = zc.recipe.egg
+interpreter = devpython
+eggs = traject
+
+[test]
+recipe = zc.recipe.testrunner
+eggs = traject
+defaults = ['--tests-pattern', '^f?tests$', '-v']

Added: traject/trunk/scripts/performance.py
===================================================================
--- traject/trunk/scripts/performance.py	                        (rev 0)
+++ traject/trunk/scripts/performance.py	2009-10-23 15:27:49 UTC (rev 105243)
@@ -0,0 +1,43 @@
+import time
+import traject
+
+class Root(object):
+    pass
+
+class Department(object):
+    def __init__(self, department_id):
+        self.department_id = department_id
+
+class Employee(object):
+    def __init__(self, department_id, employee_id):
+        self.department_id = department_id
+        self.employee_id = employee_id
+
+class Default(object):
+    def __init__(self, **kw):
+        self.kw = kw
+
+def main():    
+    patterns = traject.Patterns()
+    patterns.register(Root,
+                      'departments/:department_id/employees/:employee_id',
+                      Employee)
+    patterns.register(Root,
+                      'departments/:department_id',
+                      Department)
+    amount = 1000
+    root = Root()
+    s = time.time()
+
+    for i in range(amount):
+        obj = patterns.resolve(root, 'departments/1/employees/2', Default)
+    
+    e = time.time()
+
+    elapsed = e - s
+    print "elapsed:", elapsed
+    print "resolutions per second:", amount / elapsed
+    print "time per resolution:", elapsed / amount
+    
+if __name__ == '__main__':
+    main()

Added: traject/trunk/setup.py
===================================================================
--- traject/trunk/setup.py	                        (rev 0)
+++ traject/trunk/setup.py	2009-10-23 15:27:49 UTC (rev 105243)
@@ -0,0 +1,39 @@
+import os
+from setuptools import setup, find_packages
+
+version = '0.1dev'
+
+def read(*filenames):
+    return open(os.path.join(os.path.dirname(__file__), *filenames)).read()
+
+setup(name='traject',
+      version=version,
+      description="A URL dispatch to object system that combines aspects of routing and traversal.",
+      # long_description=read('README.txt'),
+      # Use classifiers that are already listed at:
+      # http://pypi.python.org/pypi?%3Aaction=list_classifiers
+      classifiers=['Development Status :: 5 - Production/Stable',
+                   'Environment :: Web Environment',
+                   'Framework :: Zope3',
+                   'Intended Audience :: Developers',
+                   'License :: OSI Approved :: Zope Public License',
+                   'Operating System :: OS Independent',
+                   'Programming Language :: Python',
+                   'Topic :: Internet :: WWW/HTTP',
+                   'Topic :: Software Development :: Libraries',
+                   ],
+      keywords="route routing url traverse traversing web",
+      author="Martijn Faassen",
+      author_email="faassen at startifact.com",
+      license="ZPL",
+      package_dir={'': 'src'},
+      packages=find_packages('src'),
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=['setuptools',
+                        'zope.interface',
+                        ],
+      entry_points="""
+      # Add entry points here
+      """,
+      )

Added: traject/trunk/src/traject/__init__.py
===================================================================
--- traject/trunk/src/traject/__init__.py	                        (rev 0)
+++ traject/trunk/src/traject/__init__.py	2009-10-23 15:27:49 UTC (rev 105243)
@@ -0,0 +1,8 @@
+from traject._traject import parse, subpatterns, Patterns
+from traject._traject import (register, register_inverse,
+                              resolve, resolve_stack,
+                              consume, consume_stack,
+                              locate)
+from traject._traject import (ParseError, RegistrationError,
+                              ResolutionError, LocationError)
+

Added: traject/trunk/src/traject/_traject.py
===================================================================
--- traject/trunk/src/traject/_traject.py	                        (rev 0)
+++ traject/trunk/src/traject/_traject.py	2009-10-23 15:27:49 UTC (rev 105243)
@@ -0,0 +1,257 @@
+import re
+from urllib import unquote
+
+from zope.interface import implementedBy, providedBy
+from zope.interface.interfaces import ISpecification
+from zope.interface.adapter import AdapterRegistry
+from zope.interface import Interface
+
+class IStep(Interface):
+    pass
+
+class IFactory(Interface):
+    pass
+
+class IInverse(Interface):
+    pass
+
+class ParseError(Exception):
+    """Raised if there is a problem parsing an URL pattern.
+    """
+
+class RegistrationError(Exception):
+    """Raised if there is a problem registering an URL pattern.
+    """
+
+class ResolutionError(Exception):
+    """Raised if there was a problem resolving a path.
+    """
+
+class LocationError(Exception):
+    """Raised if there was a problem reconstructing a location.
+    """
+    
+def parse(pattern_str):
+    """Parse an URL pattern.
+
+    Takes a URL pattern string and parses it into a tuple. Pattern
+    strings look like this: foo/:bar/baz
+
+    pattern_str - the pattern
+
+    returns the pattern tuple.
+    """
+    pattern_str = normalize(pattern_str)
+    result = []
+    pattern = tuple(pattern_str.split('/'))
+    known_variables = set()
+    for step in pattern:
+        if step[0] == ':':
+            if step in known_variables:
+                raise ParseError(
+                    'URL pattern contains multiple variables with name: %s' %
+                    step[1:])
+            known_variables.add(step)
+    return pattern
+
+def subpatterns(pattern):
+    """Decompose a pattern into sub patterns.
+
+    A pattern can be decomposed into a number of sub patterns.
+    ('a', 'b', 'c') for instance has the sub patterns ('a',),
+    ('a', 'b') and ('a', 'b', 'c').
+
+    pattern - the pattern tuple to decompose.
+
+    returns the sub pattern tuples of this pattern.
+    """
+    subpattern = []
+    result = []
+    for step in pattern:
+        subpattern.append(step)
+        result.append(tuple(subpattern))
+    return result
+
+def generalize_pattern(pattern):
+    result = []
+    for p in pattern:
+        if p[0] == ':':
+            result.append(':')
+        else:
+            result.append(p)
+    return tuple(result)
+
+def component_name(pattern):
+    return '/'.join(generalize_pattern(pattern))
+    
+def _get_interface(class_or_interface):
+    if ISpecification.providedBy(class_or_interface):
+        return class_or_interface
+    else:
+        return implementedBy(class_or_interface)
+
+_dummy = object()
+
+class Patterns(object):
+    def __init__(self):
+        self._registry = AdapterRegistry()
+        self._inverse_registry = AdapterRegistry()
+        
+    def register(self, class_or_interface, pattern_str, factory):
+        interface = _get_interface(class_or_interface)
+        pattern = parse(pattern_str)
+        sp = subpatterns(pattern)
+        for p in sp:
+            name = component_name(p)
+            if name[-1] == ':':
+                value = p[-1][1:]
+                prev_value = self._registry.registered(
+                    (interface,), IStep, name)
+                if prev_value == value:
+                    continue
+                if prev_value is not None:
+                    raise RegistrationError(
+                        "Could not register %s because of a conflict "
+                        "between variable %s and already registered %s" %
+                        ('/'.join(pattern), value, prev_value))
+            else:
+                value = _dummy
+            self._registry.register((interface,), IStep, name, value)
+        p = sp[-1]
+        name = component_name(p)
+        self._registry.register((interface,), IFactory, name, factory)
+
+    def register_inverse(self,
+                         root_class_or_interface, model_class_or_interface,
+                         pattern_str, inverse):
+        
+        self._inverse_registry.register(
+            (_get_interface(root_class_or_interface),
+             _get_interface(model_class_or_interface)),
+            IInverse, u'',
+            (parse(pattern_str), inverse))
+    
+    def resolve(self, root, path, default_factory):
+        path = normalize(path)
+        names = path.split('/')
+        names.reverse()
+        return self.resolve_stack(root, names, default_factory)
+
+    def resolve_stack(self, root, stack, default_factory):
+        unconsumed, obj = self.consume_stack(root, stack, default_factory)
+        if unconsumed:
+            raise ResolutionError(
+                "Could not resolve path: %s" % '/'.join(reversed(stack)))
+        return obj
+
+    def consume(self, root, path, default_factory):
+        path = normalize(path)
+        names = path.split('/')
+        names.reverse()
+        return self.consume_stack(root, names, default_factory)
+
+    def consume_stack(self, root, stack, default_factory):
+        variables = {}
+        provided = (providedBy(root),)
+        obj = root
+        pattern = ()
+        while stack:
+            name = stack.pop()
+            step_pattern = pattern + (name,)
+            step_pattern_str = '/'.join(step_pattern)
+            # check whether we can make a next step
+            next_step = self._registry.lookup(provided, IStep,
+                                              step_pattern_str)
+            
+            if next_step is not None:
+                # if so, that's the pattern we matched
+                pattern = step_pattern
+                pattern_str = step_pattern_str
+            else:
+                # if not, see whether we can match a variable
+                variable_pattern = pattern + (':',)
+                variable_pattern_str = '/'.join(variable_pattern)
+                variable = self._registry.lookup(provided, IStep,
+                                                 variable_pattern_str)
+                if variable is not None:
+                    # if so, we matched the variable pattern
+                    pattern = variable_pattern
+                    pattern_str = variable_pattern_str
+                    # the variable name is registered
+                    variables[str(variable)] = name
+                else:
+                    # cannot find step or variable, so cannot resolve
+                    stack.append(name)
+                    return stack, obj
+            # now see about constructing the object
+            factory = self._registry.lookup(provided, IFactory, pattern_str)
+            if factory is None:
+                factory = default_factory
+            parent = obj
+            obj = factory(**variables)    
+            if obj is None:
+                stack.append(name)
+                # we cannot resolve to a factory that returns None
+                return stack, parent
+            obj.__name__ = name
+            obj.__parent__ = parent
+        return stack, obj
+
+    def locate(self, root, model, default):
+        if hasattr(model, '__parent__') and model.__parent__ is not None:
+            return
+    
+        root_interface = providedBy(root)
+        model_interface = providedBy(model)
+        v = self._inverse_registry.lookup(
+            (root_interface, model_interface), IInverse, name=u'')
+        if v is None:
+            raise LocationError("Cannot reconstruct parameters of: %s" % model)
+        pattern, inverse = v
+        gen_pattern = generalize_pattern(pattern)
+
+        # obtain the variables
+        variables = inverse(model)
+        variables = dict([(str(key), value) for (key, value) in
+                          variables.items()])
+        pattern = list(pattern)
+        gen_pattern = list(gen_pattern)
+        while True:
+            name = pattern.pop()                
+            gen_name = gen_pattern.pop()
+    
+            if gen_name == ':':
+                name = name[1:]
+                name = variables.pop(name)  
+            model.__name__ = name
+    
+            # no more parents we can find, so we're at the root
+            if not gen_pattern:
+                model.__parent__ = root
+                return
+            factory = self._registry.lookup(
+                (root_interface,), IFactory, name='/'.join(gen_pattern))
+            
+            if factory is None:
+                factory = default
+            parent = factory(**variables)
+            model.__parent__ = parent
+            model = parent
+            
+            if hasattr(model, '__parent__') and model.__parent__ is not None:
+                # we're done, as parent as a parent itself
+                return
+
+def normalize(pattern_str):
+    if pattern_str.startswith('/'):
+        return pattern_str[1:]
+    return pattern_str
+
+_patterns = Patterns()
+register = _patterns.register
+register_inverse = _patterns.register_inverse
+resolve = _patterns.resolve
+resolve_stack = _patterns.resolve_stack
+consume = _patterns.consume
+consume_stack = _patterns.consume_stack
+locate = _patterns.locate

Added: traject/trunk/src/traject/tests.py
===================================================================
--- traject/trunk/src/traject/tests.py	                        (rev 0)
+++ traject/trunk/src/traject/tests.py	2009-10-23 15:27:49 UTC (rev 105243)
@@ -0,0 +1,867 @@
+import unittest, doctest
+
+from zope.interface import Interface, implements
+
+import traject
+
+class Default(object):
+    def __init__(self, kw):
+        self.kw = kw
+
+def default(**kw):
+    return Default(kw)
+
+class Root(object):
+    pass
+
+class SubRoot(Root):
+    pass
+
+class Department(object):
+    def __init__(self, department_id):
+        self.department_id = department_id
+
+class SubDepartment(Department):
+    pass
+
+class Employee(object):
+    def __init__(self, department_id, employee_id):
+        self.department_id = department_id
+        self.employee_id = employee_id
+
+class SubEmployee(Employee):
+    pass
+
+class EmployeeData(object):
+    def __init__(self, department_id, employee_id):
+        self.department_id = department_id
+        self.employee_id = employee_id
+
+class SpecialDepartment(object):
+    def __init__(self):
+        pass
+
+class SpecialEmployee(object):
+    def __init__(self, employee_id):
+        self.employee_id = employee_id
+        
+class PatternsTestCase(unittest.TestCase):
+
+    def test_simple_steps(self):
+        self.assertEquals(
+            ('a', 'b', 'c'),
+            traject.parse('a/b/c'))
+    
+    def test_simple_steps_starting_slash(self):
+        self.assertEquals(
+            ('a', 'b', 'c'),
+            traject.parse('/a/b/c'))
+
+    def test_steps_with_variable(self):
+        self.assertEquals(
+            ('a', ':B', 'c'),
+            traject.parse('a/:B/c'))
+
+    def test_steps_with_double_variable_name(self):
+        self.assertRaises(
+            traject.ParseError,
+            traject.parse, 'a/:B/c/:B')
+
+    def test_subpatterns(self):
+        self.assertEquals(
+            [('a',),
+             ('a', ':B'),
+             ('a', ':B', 'c'),
+             ],
+            traject.subpatterns(
+                ('a', ':B', 'c')
+                ))
+
+    def get_patterns(self):
+        patterns = traject.Patterns()
+        class Obj(object):
+            def __init__(self, b, d):
+                self.b = b
+                self.d = d
+        def f(b, d):
+            return Obj(b, d)
+        patterns.register(Root, 'a/:b/c/:d', f)
+        return patterns, Obj
+    
+    def test_patterns_resolve_full_path(self):
+        patterns, Obj = self.get_patterns()
+        root = Root()
+        
+        # now that we have a pattern registered, we can try it
+        obj = patterns.resolve(root, 'a/B/c/D', default)
+
+        # we see that the parents and names are correct
+        self.assertEquals('D', obj.__name__)
+        self.assert_(isinstance(obj, Obj))
+        obj = obj.__parent__
+        self.assertEquals('c', obj.__name__)
+        self.assert_(isinstance(obj, Default))
+        obj = obj.__parent__
+        self.assertEquals('B', obj.__name__)
+        self.assert_(isinstance(obj, Default))
+        obj = obj.__parent__
+        self.assertEquals('a', obj.__name__)
+        self.assert_(isinstance(obj, Default))
+        obj = obj.__parent__
+        self.assert_(isinstance(obj, Root))
+
+    def test_patterns_resolve_stack_full_path(self):
+        patterns, Obj = self.get_patterns()
+        root = Root()
+
+        l = ['a', 'B', 'c', 'D']
+        l.reverse()
+
+        obj = patterns.resolve_stack(root, l, default)
+
+        # we see that the parents and names are correct
+        self.assertEquals('D', obj.__name__)
+        self.assert_(isinstance(obj, Obj))
+        obj = obj.__parent__
+        self.assertEquals('c', obj.__name__)
+        self.assert_(isinstance(obj, Default))
+        obj = obj.__parent__
+        self.assertEquals('B', obj.__name__)
+        self.assert_(isinstance(obj, Default))
+        obj = obj.__parent__
+        self.assertEquals('a', obj.__name__)
+        self.assert_(isinstance(obj, Default))
+        obj = obj.__parent__
+        self.assert_(isinstance(obj, Root))
+
+    def test_patterns_consume_stack_full_path(self):
+        patterns, Obj = self.get_patterns()
+        root = Root()
+
+        l = ['a', 'B', 'c', 'D']
+        l.reverse()
+
+        unconsumed, obj = patterns.consume_stack(root, l, default)
+
+        # have nothing left unconsumed
+        self.assertEquals([], unconsumed)
+        
+        # we see that the parents and names are correct
+        self.assertEquals('D', obj.__name__)
+        self.assert_(isinstance(obj, Obj))
+        obj = obj.__parent__
+        self.assertEquals('c', obj.__name__)
+        self.assert_(isinstance(obj, Default))
+        obj = obj.__parent__
+        self.assertEquals('B', obj.__name__)
+        self.assert_(isinstance(obj, Default))
+        obj = obj.__parent__
+        self.assertEquals('a', obj.__name__)
+        self.assert_(isinstance(obj, Default))
+        obj = obj.__parent__
+        self.assert_(isinstance(obj, Root))
+
+    def test_patterns_resolve_partial_path(self):
+        patterns, Obj = self.get_patterns()
+        root = Root()
+        
+        # let's try resolving a partial path
+        obj = patterns.resolve(root, 'a/B/c', default)
+
+        # we get a bunch of default objects
+        self.assertEquals('c', obj.__name__)
+        self.assert_(isinstance(obj, Default))
+        obj = obj.__parent__
+        self.assertEquals('B', obj.__name__)
+        self.assert_(isinstance(obj, Default))
+        obj = obj.__parent__
+        self.assertEquals('a', obj.__name__)
+        self.assert_(isinstance(obj, Default))
+        obj = obj.__parent__
+        self.assert_(isinstance(obj, Root))
+
+    def test_patterns_consume_tack_partial_path(self):
+        patterns, Obj = self.get_patterns()
+        root = Root()
+        
+        # let's try resolving a partial path
+        l = ['a', 'B', 'c']
+        l.reverse()
+        
+        unconsumed, obj = patterns.consume_stack(root, l, default)
+
+        self.assertEquals([], unconsumed)
+    
+        # we get a bunch of default objects
+        self.assertEquals('c', obj.__name__)
+        self.assert_(isinstance(obj, Default))
+        obj = obj.__parent__
+        self.assertEquals('B', obj.__name__)
+        self.assert_(isinstance(obj, Default))
+        obj = obj.__parent__
+        self.assertEquals('a', obj.__name__)
+        self.assert_(isinstance(obj, Default))
+        obj = obj.__parent__
+        self.assert_(isinstance(obj, Root))
+
+    def test_patterns_resolve_impossible_path(self):
+        patterns, Obj = self.get_patterns()
+        root = Root()
+        
+        # let's try resolving something that doesn't go anywhere
+        self.assertRaises(traject.ResolutionError,
+                          patterns.resolve, root, 'b/c/d', default)
+
+    def test_patterns_resolve_stack_impossible_path(self):
+        patterns, Obj = self.get_patterns()
+        root = Root()
+        
+        # let's try resolving something that doesn't go anywhere
+        l = ['b', 'c', 'd']
+        l.reverse()
+        self.assertRaises(traject.ResolutionError,
+                          patterns.resolve_stack, root, l, default)
+
+    def test_patterns_consume_stack_impossible_path(self):
+        patterns, Obj = self.get_patterns()
+        root = Root()
+
+        l = ['b', 'c', 'd']
+        l.reverse()
+        
+        unconsumed, obj = patterns.consume_stack(root, l, default)
+
+        self.assert_(obj is root)
+        self.assertEquals(['d', 'c', 'b'], unconsumed)
+
+    def test_wrong_arguments(self):
+        patterns = traject.Patterns()
+        class Obj(object):
+            def __init__(self, b, d):
+                self.b = b
+                self.d = d
+        # different arguments than expected from path
+        def f(a, c):
+            return Obj(a, c)
+        patterns.register(Root, 'a/:b/c/:d', f)
+
+        root = Root()
+    
+        # let's try resolving it
+        self.assertRaises(TypeError, patterns.resolve, root, 'a/B/c/D', default)
+
+    def test_resolve_to_factory_that_returns_none(self):
+        patterns = traject.Patterns()
+        class Model(object):
+            def __init__(self, id):
+                self.id = id
+        def factory(id):
+            # only return a model if the id is an int
+            try:
+                id = int(id)
+            except ValueError:
+                return None
+            return Model(id)
+        patterns.register(Root, 'models/:id', factory)
+        root = Root()
+
+        self.assertRaises(traject.ResolutionError, patterns.resolve,
+                          root, 'models/not_an_int', default)
+        
+    def test_consume_to_factory_that_returns_none(self):
+        patterns = traject.Patterns()
+        class Model(object):
+            def __init__(self, id):
+                self.id = id
+        def factory(id):
+            # only return a model if the id is an int
+            try:
+                id = int(id)
+            except ValueError:
+                return None
+            return Model(id)
+        patterns.register(Root, 'models/:id', factory)
+        root = Root()
+
+        unconsumed, obj = patterns.consume(root, 'models/not_an_int', default)
+        self.assertEquals(['not_an_int'], unconsumed)
+        self.assertEquals('models', obj.__name__)
+        self.assert_(obj.__parent__ is root)
+        
+    def get_multi_patterns(self):
+        patterns = traject.Patterns()
+        patterns.register(Root, 'departments/:department_id',
+                          Department)
+        patterns.register(Root,
+                          'departments/:department_id/employees/:employee_id',
+                          Employee)
+        return patterns
+    
+    def test_multiple_registrations_resolve_to_child(self):
+        patterns = self.get_multi_patterns()
+        root = Root()
+
+        obj = patterns.resolve(root, u'departments/1/employees/10', default)
+
+        self.assert_(isinstance(obj, Employee))
+        self.assertEquals(u'1', obj.department_id)
+        self.assertEquals(u'10', obj.employee_id)
+
+    def test_multiple_registrations_consume_to_child_and_view(self):
+        patterns = self.get_multi_patterns()
+        root = Root()
+
+        unconsumed, obj = patterns.consume(
+            root, u'departments/1/employees/10/index',
+            default)
+
+        self.assertEquals(['index'], unconsumed)
+        
+        self.assert_(isinstance(obj, Employee))
+        self.assertEquals(u'1', obj.department_id)
+        self.assertEquals(u'10', obj.employee_id)
+
+    def test_multiple_registrations_resolve_to_parent(self):
+        patterns = self.get_multi_patterns()
+        root = Root()
+    
+        obj = patterns.resolve(root, u'departments/1', default)
+
+        self.assert_(isinstance(obj, Department))
+        self.assertEquals(u'1', obj.department_id)
+
+    def test_multiple_registrations_consume_to_parent_and_view(self):
+        patterns = self.get_multi_patterns()
+        root = Root()
+    
+        unconsumed, obj = patterns.consume(
+            root, u'departments/1/index', default)
+
+        self.assertEquals(['index'], unconsumed)
+        
+        self.assert_(isinstance(obj, Department))
+        self.assertEquals(u'1', obj.department_id)
+
+    def test_multiple_registrations_resolve_to_nonexistent(self):
+        patterns = self.get_multi_patterns()
+        root = Root()
+
+        # we will also test resolving a URL that doesn't work at all
+        self.assertRaises(traject.ResolutionError,
+                          patterns.resolve, root, u'foo/1/bar', default)
+
+    def get_overlapping_patterns(self):
+        patterns = self.get_multi_patterns()
+        # we register two things for the same path, a variable and a step
+        patterns.register(Root,
+                          'departments/special',
+                          SpecialDepartment)
+        return patterns
+    
+    def test_overlapping_variable_and_step_resolve_to_child(self):
+        patterns = self.get_overlapping_patterns()
+        root = Root()
+        
+        obj = patterns.resolve(root, u'departments/1/employees/10', default)
+
+        self.assert_(isinstance(obj, Employee))
+        self.assertEquals(u'1', obj.department_id)
+        self.assertEquals(u'10', obj.employee_id)
+
+    def test_overlapping_variable_and_step_resolve_to_parent(self):
+        patterns = self.get_overlapping_patterns()
+        root = Root()
+                
+        obj = patterns.resolve(root, u'departments/1', default)
+
+        self.assert_(isinstance(obj, Department))
+        self.assertEquals(u'1', obj.department_id)
+
+    def test_overlapping_variable_and_step_resolve_to_step(self):
+        patterns = self.get_overlapping_patterns()
+        root = Root()
+       
+        # we can also get to the special department. the step will have
+        # priority over the variable
+        obj = patterns.resolve(root, u'departments/special', default)
+        self.assert_(isinstance(obj, SpecialDepartment))
+
+    def test_overlapping_variable_and_step_resolve_to_step_nonexistent(self):
+        patterns = self.get_overlapping_patterns()
+        root = Root()
+ 
+        # sub-paths from special haven't been registered, so don't exist
+        self.assertRaises(
+            traject.ResolutionError,
+            patterns.resolve, root, u'/departments/special/employees/10',
+            default)
+
+    def test_overlapping_variable_and_step_resolve_to_step_subpath(self):
+        patterns = self.get_overlapping_patterns()
+        root = Root()
+       
+        patterns.register(Root,
+                          'departments/special/employees/:employee_id',
+                          SpecialEmployee)
+        obj = patterns.resolve(root, u'/departments/special/employees/10',
+                               default)
+        self.assert_(isinstance(obj, SpecialEmployee))
+
+
+    def test_factory_override(self):
+        patterns = self.get_multi_patterns()
+        # for SubRoot we use a different factory
+        patterns.register(SubRoot,
+                          'departments/:department_id/employees/:employee_id',
+                         SubEmployee)
+       
+        subroot = SubRoot()
+        
+        obj = patterns.resolve(subroot,
+                               u'departments/1/employees/10', default)
+
+        self.assert_(obj.__class__ is SubEmployee)
+        self.assertEquals(u'1', obj.department_id)
+        self.assertEquals(u'10', obj.employee_id)
+
+    def test_factory_override_root_stays_the_same(self):
+        patterns = self.get_multi_patterns()
+        # for SubRoot we use a different factory
+        patterns.register(SubRoot,
+                          'departments/:department_id/employees/:employee_id',
+                          SubEmployee)
+       
+        root = Root()
+
+        # we don't resolve for subroot but for root
+        obj = patterns.resolve(root,
+                               u'departments/1/employees/10', default)
+        
+        self.assert_(obj.__class__ is Employee)
+        self.assertEquals(u'1', obj.department_id)
+        self.assertEquals(u'10', obj.employee_id)
+
+    def test_factory_extra_path(self):
+        patterns = self.get_multi_patterns()
+        # for SubRoot we add a path
+        patterns.register(
+            SubRoot,
+            'departments/:department_id/employees/:employee_id/data',
+            EmployeeData)
+
+        subroot = SubRoot()
+        
+        obj = patterns.resolve(subroot,
+                               u'departments/1/employees/10/data', default)
+
+        self.assert_(obj.__class__ is EmployeeData)
+        self.assertEquals(u'1', obj.department_id)
+        self.assertEquals(u'10', obj.employee_id)
+
+    def test_factory_extra_path_absent_with_root(self):
+        patterns = self.get_multi_patterns()
+        # for SubRoot we add a path
+        patterns.register(
+            SubRoot,
+            'departments/:department_id/employees/:employee_id/data',
+            EmployeeData)
+
+        root = Root()
+        
+        self.assertRaises(traject.ResolutionError,
+                          patterns.resolve, root,
+                          u'departments/1/employees/10/data', default)
+
+    def test_factory_override_in_mid_path(self):
+        patterns = self.get_multi_patterns()
+        patterns.register(
+            SubRoot,
+            'departments/:department_id',
+            SubDepartment)
+
+        subroot = SubRoot()
+        
+        obj = patterns.resolve(subroot,
+                               u'departments/1/employees/10', default)
+
+        employees = obj.__parent__
+        department = employees.__parent__
+        self.assert_(department.__class__ is
+                     SubDepartment)
+
+    def test_factory_original_in_mid_path(self):
+        patterns = self.get_multi_patterns()
+        patterns.register(
+            SubRoot,
+            'departments/:department_id',
+            SubDepartment)
+
+        root = Root()
+        
+        obj = patterns.resolve(root,
+                               u'departments/1/employees/10', default)
+
+        employees = obj.__parent__
+        department = employees.__parent__
+        self.assert_(department.__class__ is
+                     Department)
+
+    def test_conflicting_variable_names(self):
+        patterns = traject.Patterns()
+        patterns.register(
+            Root, 'departments/:department_id',
+            Department)
+        self.assertRaises(
+            traject.RegistrationError,
+            patterns.register,
+            Root, 'departments/:other_id', Department)
+        self.assertRaises(
+            traject.RegistrationError,
+            patterns.register,
+            Root, 'departments/:other_id/employees/:employee_id', Employee)
+
+    def test_override_variable_names(self):
+        patterns = traject.Patterns()
+        patterns.register(
+            Root, 'departments/:department_id',
+            Department)
+        class OtherDepartment(object):
+            def __init__(self, other_id):
+                self.other_id = other_id
+        patterns.register(
+            SubRoot, 'departments/:other_id',
+            OtherDepartment)
+        root = Root()
+        subroot = SubRoot()
+        department = patterns.resolve(
+            root,
+            u'departments/1', default)
+        other_department = patterns.resolve(
+            subroot,
+            u'departments/1', default)
+        
+        self.assert_(Department == department.__class__)
+        self.assert_(OtherDepartment == other_department.__class__)
+        self.assertEquals(u'1', other_department.other_id)
+
+    def test_conflict_in_override_variable_names(self):
+        patterns = traject.Patterns()
+        patterns.register(
+            Root, 'departments/:department_id',
+            Department)
+        class OtherDepartment(object):
+            def __init__(self, other_id):
+                self.other_id = other_id
+        patterns.register(
+            SubRoot, 'departments/:other_id',
+            OtherDepartment)
+        patterns.register(
+            Root, 'departments/:department_id/employees/:employee_id',
+            Employee)
+        subroot = SubRoot()
+        # we cannot create Department because we got an other_id..
+        self.assertRaises(TypeError,
+                          patterns.resolve,
+                          subroot, 'departments/1/employees/2', default)
+        
+    def test_resolved_conflict_in_override_variable_names(self):
+        patterns = traject.Patterns()
+        patterns.register(
+            Root, 'departments/:department_id',
+            Department)
+        class OtherDepartment(object):
+            def __init__(self, other_id):
+                self.other_id = other_id
+        patterns.register(
+            SubRoot, 'departments/:other_id',
+            OtherDepartment)
+        patterns.register(
+            Root, 'departments/:department_id/employees/:employee_id',
+            Employee)
+
+        # this sets up a conflicting situation when patterns are
+        # resolved from subroot. We can resolve it by another registration
+        class OtherEmployee(object):
+            def __init__(self, other_id, employee_id):
+                self.other_id = other_id
+                self.employee_id = employee_id
+        patterns.register(
+            SubRoot, 'departments/:other_id/employees/:employee_id',
+            OtherEmployee)
+        
+        subroot = SubRoot()
+        obj = patterns.resolve(
+            subroot, 'departments/1/employees/2', default)
+        self.assert_(OtherEmployee is obj.__class__)
+        self.assert_(OtherDepartment is obj.__parent__.__parent__.__class__)
+
+    def test_register_pattern_on_interface(self):
+        patterns = traject.Patterns()
+        class ISpecialRoot(Interface):
+            pass
+        class SpecialRoot(object):
+            implements(ISpecialRoot)
+        class Obj(object):
+            def __init__(self, b, d):
+                self.b = b
+                self.d = d
+        patterns.register(ISpecialRoot, 'a/:b/c/:d', Obj)
+
+        special_root = SpecialRoot()
+        obj = patterns.resolve(
+            special_root, 'a/B/c/D', default)
+        self.assert_(isinstance(obj, Obj))
+        self.assertEquals('B', obj.b)
+        self.assertEquals('D', obj.d)
+    
+
+    # XXX need a test for trailing slash?
+
+    # XXX could already introspect function to see whether we can properly
+    # register it?
+
+# factories that can retain previously created employees and departments
+_department = {}
+_employee = {}
+_departments = None
+_employees = None
+# we can keep a record of how many times things were called
+_calls = []
+
+class Departments(object):
+    pass
+
+def identityDepartments():
+    _calls.append("departments")
+    global _departments
+    if _departments is not None:
+        return _departments
+    _departments = Departments()
+    return _departments
+
+def identityDepartment(department_id):
+    _calls.append("department %s" % department_id)
+    department = _department.get(department_id)
+    if department is None:
+        _department[department_id] = department = Department(department_id)
+    return department
+
+class Employees(object):
+    def __init__(self, department_id):
+        self.department_id = department_id
+
+def identityEmployees(department_id):
+    _calls.append("employees %s" % department_id)
+    global _employees
+    if _employees is not None:
+        return _employees
+    _employees = Employees(department_id)
+    return _employees
+
+def identityEmployee(department_id, employee_id):
+    _calls.append("employee %s %s" % (department_id, employee_id))
+    employee = _employee.get((department_id, employee_id))
+    if employee is None:
+        _employee[(department_id, employee_id)] = employee = Employee(
+            department_id, employee_id)
+    return employee
+
+class InverseTestCase(unittest.TestCase):
+    def setUp(self):
+        global _departments
+        _departments = None
+        global department
+        _department.clear()
+        global _employees
+        _employees = None
+        global employee
+        _employee.clear()
+        global _calls
+        _calls = []
+  
+    def get_identity_patterns(self):
+        patterns = traject.Patterns()
+        patterns.register(Root, 'departments/:department_id',
+                          identityDepartment)
+        patterns.register(Root,
+                          'departments/:department_id/employees/:employee_id',
+                          identityEmployee)
+        patterns.register(Root, 'departments',
+                          identityDepartments)
+        patterns.register(Root,
+                          'departments/:department_id/employees',
+                          identityEmployees)
+    
+        def employee_arguments(employee):
+            return dict(employee_id=employee.employee_id,
+                        department_id=employee.department_id)
+    
+        patterns.register_inverse(
+            Root,
+            Employee,
+            u'departments/:department_id/employees/:employee_id',
+            employee_arguments)
+
+        return patterns
+
+    def get_identity_patterns_complete(self):
+        patterns = traject.Patterns()
+        patterns.register(Root, 'departments/:department_id',
+                          identityDepartment)
+        patterns.register(Root,
+                          'departments/:department_id/employees/:employee_id',
+                          identityEmployee)
+        patterns.register(Root, 'departments',
+                          identityDepartments)
+        patterns.register(Root,
+                          'departments/:department_id/employees',
+                          identityEmployees)
+    
+        def employee_arguments(employee):
+            return dict(employee_id=employee.employee_id,
+                        department_id=employee.department_id)
+        def employees_arguments(employees):
+            return dict(department_id=employees.department_id)
+        def department_arguments(department):
+            return dict(department_id=department.department_id)
+        def departments_arguments(departments):
+            return {}
+
+        patterns.register_inverse(
+            Root,
+            Employee,
+            u'departments/:department_id/employees/:employee_id',
+            employee_arguments)
+        patterns.register_inverse(
+            Root,
+            Employees,
+            u'departments/:department_id/employees',
+            employees_arguments)
+        patterns.register_inverse(
+            Root,
+            Department,
+            u'departments/:department_id',
+            department_arguments)
+        patterns.register_inverse(
+            Root,
+            Departments,
+            u'departments',
+            departments_arguments)
+        return patterns
+        
+    def test_inverse(self):
+        patterns = self.get_identity_patterns()
+        root = Root()
+
+        employee = Employee(u'1', u'2')
+        patterns.locate(root, employee, default)
+
+        self.assertEquals(u'2', employee.__name__)
+        employees = employee.__parent__
+        self.assertEquals(u'employees', employees.__name__)
+        department = employees.__parent__
+        self.assertEquals(u'1', department.__name__)
+        self.assert_(isinstance(department, Department))
+        departments = department.__parent__
+        self.assertEquals(u'departments', departments.__name__)
+        self.assert_(root is departments.__parent__)
+
+    def test_identity(self):
+        patterns = self.get_identity_patterns()
+        root = Root()
+
+        employee1 = patterns.resolve(
+            root, u'departments/1/employees/2', default)
+        employee2 = patterns.resolve(
+            root, u'departments/1/employees/2', default)
+        self.assert_(employee1 is employee2)
+        employee3 = patterns.resolve(
+            root, u'departments/1/employees/3', default)
+        self.assert_(employee1 is not employee3)
+
+    def test_no_recreation(self):
+        patterns = self.get_identity_patterns()
+        root = Root()
+
+        # the first time we do an inverse lookup, it will recreate
+        # department
+        employee = identityEmployee(u'1', u'2')
+        patterns.locate(root, employee, default)
+        global _calls
+        _calls = []
+        # if won't create anything the second time
+        patterns.locate(root, employee, default)
+        self.assertEquals([], _calls)
+
+    def test_cannot_locate(self):
+        patterns = self.get_identity_patterns()
+        root = Root()
+
+        department = identityDepartment(u'1')
+        self.assertRaises(traject.LocationError,
+                          patterns.locate, root, department, default)
+
+    def test_no_recreation_of_departments(self):
+        patterns = self.get_identity_patterns_complete()
+        root = Root()
+
+        department = identityDepartment(u'1')
+        patterns.locate(root, department, default)
+
+        global _calls
+        _calls = []
+        # if won't recreate departments the second time
+        # as it will find a Department object with a parent
+        employee = identityEmployee(u'1', u'2')
+        patterns.locate(root, employee, default)
+        self.assertEquals([u'employee 1 2', u'employees 1', u'department 1'],
+                          _calls)
+
+    def test_no_recreation_of_department(self):
+        patterns = self.get_identity_patterns_complete()
+        root = Root()
+
+        employees = identityEmployees(u'1')
+        patterns.locate(root, employees, default)
+
+        global _calls
+        _calls = []
+        # if won't recreate department the second time
+        # as it will find a employees object with a parent
+        employee = identityEmployee(u'1', u'2')
+        patterns.locate(root, employee, default)
+        self.assertEquals([u'employee 1 2', u'employees 1'],
+                          _calls)
+
+    def test_no_recreation_of_department_after_resolve(self):
+        patterns = self.get_identity_patterns_complete()
+        root = Root()
+
+        # usually location is done after resolution, causing one lookup
+        # of a parent for each object to be located below
+        patterns.resolve(root, u'departments/1/employees', default)
+        
+        global _calls
+        _calls = []
+        # if won't recreate department the second time
+        # as it will find a employees object with a parent
+        employee = identityEmployee(u'1', u'2')
+        patterns.locate(root, employee, default)
+        self.assertEquals([u'employee 1 2', u'employees 1'],
+                          _calls)
+    # test behavior of interfaces and overrides with inverse
+
+
+def test_suite():
+    optionflags=(doctest.ELLIPSIS+
+                 doctest.NORMALIZE_WHITESPACE+
+                 doctest.REPORT_NDIFF)
+    
+    suite = unittest.TestSuite()
+    suite.addTests([
+            unittest.makeSuite(PatternsTestCase),
+            unittest.makeSuite(InverseTestCase),
+            doctest.DocFileSuite('traject.txt',
+                                 optionflags=optionflags)
+            ])
+    return suite

Added: traject/trunk/src/traject/traject.txt
===================================================================
--- traject/trunk/src/traject/traject.txt	                        (rev 0)
+++ traject/trunk/src/traject/traject.txt	2009-10-23 15:27:49 UTC (rev 105243)
@@ -0,0 +1,484 @@
+Traject
+=======
+
+Introduction
+------------
+
+In web application construction there are two main ways to publish
+objects to the web: routing and traversal. Both are a form of URL
+dispatch: in the end, a function or method is called as the result of
+the pattern of the URL. Both use very different methods to do so,
+however.
+
+In *routing* a mapping is made from URL patterns to controllers (or
+views). The URL pattern is used to pull information from the URLs used
+and this information is used to determine which particular callable to
+call in the end.
+
+Take for instance the URL ``departments/10/employees/17``. A URL
+pattern could declare that the arguments `10`` and ``17`` should be
+taken from this URL by the system. This information is then used by
+the controller to retrieve the correct models from the database. The
+controller then uses information in these models to contruct the
+content of the view, for instance by rendering it with a HTML
+template.
+
+In *traversal*, there is no explicit mapping of URLs to controller or
+views. Instead models are traversed step by step, guided by the URL.
+By analogy one can in Python traverse through nested dictionaries
+(``d['a']['b']['c']``), or attributes (``d.a.b.c``). In the end, a
+*view* is looked up for the final model that can be called. The view
+could be a special attribute on the model, or more sophisticated
+systems can be used separating the view from the model.
+
+The URL ``departments/10/employees/17`` would be resolved to a
+callable because there is a ``departments`` container model that
+contains ``department`` model objects.  In turn from a ``department``
+model one can traverse to the ``employees`` container, which in turn
+allows traversal to individual employees, such as employee 17. In the
+end a view is looked up for employee 17, and called.
+
+Routing is often used in combination with relational databases
+(exposed by means of an object relational mapper,
+typically). Traversal is often more convenient with in-memory object
+structures or object databases.
+
+Routing has advantages:
+
+* good way for exposing relational content that doesn't have natural
+  nesting.
+
+* explicit overview of the URL patterns in an application.
+
+* familiar as used by many frameworks.
+
+Traversal has advantages as well:
+
+* good way for exposing object content that has arbitrary nesting.
+
+* model-driven: objects come equipped with their views, which allows
+  for easier composition of application from models.
+
+* location-aware: a nested object structure can be location
+  aware. Each model can know about its parent and its name in the
+  URL. This allows for easy construction of URLs for arbitrary models.
+  In addition, security can be declared on higher levels of the
+  structure.
+
+Traject tries to combine the properties of routing and traversal in a
+single system. Traject:
+
+* looks like a routing system and has the familiarity of the routing
+  approach.
+
+* works well for exposing relational models.
+
+* lets the developer explicitly declare URL mappings.
+
+* supports arbitrary nesting.
+
+* is model-driven. Routing is to models, not to views or controllers.
+
+* is location-aware. Models are in a nested structure and are aware of
+  their parent and name, allowing model-based security declarations
+  and easy URL construction for models.
+
+Some potential drawbacks of Traject are::
+
+* Traject expects a certain regularity in its patterns. It doesn't
+  allow certain complex URL patterns where several variables are part
+  of a single step (i.e ``foo/<bar_id>-<baz_id``). Only a single
+  variable is allowed per URL segment.
+
+* Traject needs to constructs or retrieve models for *each stage* in
+  the route in order to construct a nested structure. This can mean
+  more queries to the database per request. In practice this is often
+  mitigated by the fact that the parent models in the structure are
+  frequently needed by the view logic anyway.
+
+* In Traject each model instance should have one and only one location
+  in the URL structure. This allows not only URLs to be resolved to
+  models, but also URLs to be generated for models. If you want the
+  same model to be accessible through multiple URLs, you might have
+  some difficulty.
+
+URL patterns
+------------
+
+Let's consider an URL pattern string that is a series of steps
+separated by slashes::
+
+  >>> pattern_str = 'foo/bar/baz'
+
+We can decompose it into its component steps using the ``parse``
+function::
+
+  >>> import traject
+  >>> traject.parse(pattern_str)
+  ('foo', 'bar', 'baz')
+
+Steps may also be variables. A variable is a step that is prefixed by
+the colon (``:``)::
+
+  >>> traject.parse('foo/:a/baz')
+  ('foo', ':a', 'baz')
+
+More than one variable step is allowed in a pattern::
+
+  >>> traject.parse('foo/:a/baz/:b')
+  ('foo', ':a', 'baz', ':b')
+
+Each variable in a pattern needs to have a different name, however::
+
+  >>> traject.parse('foo/:a/baz/:a')
+  Traceback (innermost last):
+    ...
+  ParseError: URL pattern contains multiple variables with name: a
+
+Registering patterns
+--------------------
+
+In Traject, the resolution of a URL path results in a model. This
+model can then in turn have views registered for it that allow this
+model to be displayed. How this works is up to the web framework's
+view system.
+
+How does Traject know which model to return for a path? You can
+register a factory function for a URL pattern. This factory function
+should create or retrieve the model object.
+
+The factory function receives parameters for each of the matched
+variables in whichever pattern matched - the signature of the factory
+function should account for all the variables in the patterns that are
+matched.
+
+Let's look at an example.
+
+This is the URL pattern we want to recognize::
+
+  >>> pattern_str = u'departments/:department_id/employees/:employee_id'
+
+We can see two parameters in this URL pattern: `department_id`` and
+``customer_id``.
+
+We now define a model object as it might be stored in a database::
+
+  >>> class Employee(object):
+  ...   def __init__(self, department_id, employee_id):
+  ...     self.department_id = department_id
+  ...     self.employee_id = employee_id
+  ...   def __repr__(self):
+  ...     return '<Employee %s %s>' % (self.department_id, self.employee_id)
+
+Now we define the factory function for this URL pattern. The
+parameters in this case would be ``department_id`` and
+``employee_id``::
+
+  >>> def factory(department_id, employee_id): 
+  ...   return Employee(department_id, employee_id)
+
+The factory function in this case just creates a ``Employee`` object
+on the fly. Instead, it could do a database query based on its parents
+and the parameters supplied.
+
+In order to register this factory function, we need a registry of patterns, so
+we'll create one::
+
+  >>> patterns = traject.Patterns()
+
+A pattern needs to be registered for a class or an interface. In this
+case we'll register the patterns for a class ``Root``::
+
+  >>> class Root(object):
+  ...    pass
+
+We can now register the URL pattern and the factory::
+
+  >>> patterns.register(Root, pattern_str, factory)
+
+Resolving a path
+----------------
+
+Now we are ready to resolve paths. A path is part of a URL such as
+``foo/bar/baz``. It looks very much like a pattern, but all the
+variables will have been filled in.
+
+The models retrieved by resolving paths will be *located*. Ultimately
+their ancestor will be a particular root model from which all paths
+are resolved. Let's create one here for demonstration purposes::
+
+  >>> root = Root()
+
+When a path is resolved, a complete chain of ancestors from model to
+root is also created. It may be that no particular factory function
+was registered for a particular path. In our current registry such
+patterns indeed exist: ``departments``, ``departments/:department_id``
+and ``departments/:department_id/employees`` all have no factory
+registered.
+
+When resolving the pattern we need to supply a special default factory
+which will generate an object in such cases. Let's make one here. The
+factory function needs to be able to deal with arbitrary keyword
+arguments as any number of parameters might be supplied::
+
+  >>> class Default(object):
+  ...     def __init__(self, **kw):
+  ...         pass
+
+We can now resolve a path::
+  
+  >>> obj = patterns.resolve(root, u'departments/1/employees/2', Default)
+  >>> obj
+  <Employee 1 2>
+
+We can also resolve a stack of names (where the first name to resolve
+is on the top of the stack)::
+
+  >>> l = [u'departments', u'1', u'employees', u'2']
+  >>> l.reverse()
+  >>> patterns.resolve_stack(root, l, Default)
+  <Employee 1 2>
+ 
+Locations
+---------
+
+Traject supports the notion of locations. After we find a model, the
+model will have received two special attributes::
+
+*  ``__name__``: the name we addressed this object with in the path
+
+* ``__parent__``: the parent of the model. This is an model that
+  matches the path without the last step.
+
+The parent will in turn have a parent as well, all the way up to the
+ultimate ancestor, the root.
+
+We can look at the object we retrieved before to demonstrate the
+ancestor chain::
+
+  >>> obj.__name__
+  u'2'
+  >>> isinstance(obj, Employee)
+  True
+  >>> p1 = obj.__parent__
+  >>> p1.__name__ 
+  u'employees'
+  >>> isinstance(p1, Default)
+  True
+  >>> p2 = p1.__parent__
+  >>> p2.__name__
+  u'1'
+  >>> isinstance(p2, Default)
+  True
+  >>> p3 = p2.__parent__
+  >>> p3.__name__
+  u'departments'
+  >>> isinstance(p3, Default)
+  True
+  >>> p3.__parent__ is root
+  True
+
+Default objects have been created for each step along the way, up
+until the root.
+
+Consuming a path
+----------------
+
+In a mixed traject/traversal environment, for instance where view
+lookup is done by traversal, it can be useful to be able to resolve a
+path according to the patterns registered until no longer
+possible. The rest of the the steps are not followed.
+
+The method consume will consume steps as far as possible, return the
+steps that weren't consumed yet, and the object it managed to find::
+
+  >>> unconsumed, last_obj = patterns.consume(root, 
+  ...       'departments/1/some_view', Default)
+  >>> unconsumed
+  ['some_view']
+  >>> isinstance(last_obj, Default)
+  True
+  >>> last_obj.__name__
+  '1'
+  >>> p1 = last_obj.__parent__ 
+  >>> p1.__name__
+  'departments'
+  >>> p1.__parent__ is root
+  True
+
+The method ``consume_stack`` does the same with a stack::
+
+  >>> l = ['departments', '1', 'some_view']
+  >>> l.reverse()
+  >>> unconsumed, last_obj = patterns.consume_stack(root, l, Default)
+  >>> unconsumed
+  ['some_view']
+  >>> isinstance(last_obj, Default)
+  True
+  >>> last_obj.__name__
+  '1'
+  >>> p1 = last_obj.__parent__ 
+  >>> p1.__name__
+  'departments'
+  >>> p1.__parent__ is root
+  True
+
+Giving a model its location
+---------------------------
+
+When we retrieve a model directly by means of a query or construction
+in our application code (such as in our view logic) it will not have a
+location. This is inconvenient if we want to ask the model questions
+that need such location information (such as its URL or its
+location-dependent permissions).
+
+We can register a special function per model class that is the inverse
+of the factory. Given a model instance, it needs to return the
+arguments used in the pattern towards it::
+
+  >>> def employee_arguments(obj):
+  ...     return {'employee_id': obj.employee_id, 
+  ...             'department_id': obj.department_id} 
+  >>> patterns.register_inverse(Root, Employee, pattern_str, employee_arguments)
+
+Let's construct some employee now::
+
+  >>> m = Employee(u'13', u'27')
+
+It has no location (no ``__name__`` or ``__parent__``)::
+
+  >>> m.__name__
+  Traceback (most recent call last):
+    ...
+  AttributeError: ...
+
+  >>> m.__parent__
+  Traceback (most recent call last):
+    ...
+  AttributeError: ...
+
+We can now locate it::
+
+  >>> patterns.locate(root, m, Default)
+
+The model will now have ``__name__`` and ``__parent__`` attributes::
+
+  >>> m.__name__
+  u'27'
+  >>> p1 = m.__parent__
+  >>> p1.__name__
+  u'employees'
+  >>> p2 = p1.__parent__
+  >>> p2.__name__
+  u'13'
+  >>> p3 = p2.__parent__
+  >>> p3.__name__
+  u'departments'
+  >>> p3.__parent__ is root
+  True
+
+Problems:
+
+* how do we turn this into traversal? The classes of the objects
+  traversed to need to have traversers registered for them just in
+  time? Alternatively write a traversal mechanic that delivers the
+  object while eating as much of the path as possible, taking the last
+  bit as the view?
+
+* how do we know traversing ends? actually each step of the traversal
+  returns a model. The traverser registered for each model checks for
+  the view itself.
+
+* when the route traverser is in place, it will determine traversal
+  based on what is traversed already and the next step in the
+  traversal. This means that this traversal can be universal for the
+  entire route traversing process. Each route traversing object can be
+  provided with an interface that this traverser adapts to, during 
+  traversal. This can be done just in time.
+
+* URL parameters -> object: given parameters, return object inverse:
+  object -> URL parameters: given object, give back URL parameters
+  this allows us to reconstruct the parent trail, though there is a
+  cost involved, and how do we make sure that every object has a
+  __parent__ and __name__ when returned from session.query()?
+
+* MapperExtension with reconstruct_instance could set __parent__ and 
+  such.
+
+* how to handle collections/relations? If we have the default
+  collection, how do we attach views? If so, can we safely put in
+  __parent__ and friends? If so, when? Or do we require the web app to
+  return a proxy-like object that does have views?
+
+* collection.on_link can be used to set parent.
+
+* is it possible to transparently install other collection proxies into
+  the mapper or something?
+
+* the performance of doing a resolve for each query result isn't very high,
+  better do something that smartly detects a __parent__ on its parent and
+  bails out early if so.
+
+* we could install a special function locatedquery which is a wrapped
+  session.query that retrieves objects from the database that are
+  wrapped.  It could take care of relation objects too (and so could
+  traversal), but what about the objects in relations and such? And so on?
+  We don't want to thrawl through everythign to wrap 'm.
+  Nicer would be a mapper extension that puts this in automatically,
+  but that might be tricky to implement...
+
+* we need a lot of tests for failures: wrong parameters, what if the
+  query raises an exception, what if the argument reconstruction
+  returns the wrong parameters, what if the query returns None, etc.
+
+* We need to figure out whether we can hijack traversal in some
+  structured way to allow multi-step traversal, or smoothly extend
+  resolve so it can work with the traversal process? using the
+  traversal stack mechanism on the request, perhaps?
+
+* reconstruct: build the object *without* parents, then see whether
+  it already *has* parents, and if so, be done. Otherwise, construct
+  parent, walking up path. Finally, connect to root.
+
+* factory function now receives parent. That's fine for resolving
+  routes, but when trying to efficiently reconstruct a parent chain
+  it's in the way: we'd need a parent in order to reconstruct a
+  parent!
+
+* can a factory return None? What happens?
+
+* can an inverse return None? what happens?
+
+class EmployeePattern(traject.Traject):
+    grok.context(AppRoot)
+    traject.pattern('departments/:department_id/employees/:employee_id')
+    traject.model(model.Employee)
+
+    def factory(department_id, employee_id):
+        return session.query(model.Employee).first()
+
+    def arguments(employee):
+        return dict(department_id=employee.department.id, 
+                    employee_id=employee.id)
+
+
+ at traject.pattern(AppRoot, 'departments/:department_id/employees/:employee_id')
+def factory(department_id, employee_id):
+     ...
+
+ at traject.inverse(AppRoot, model.Employee, 'departments/:department_id..')
+def arguments(employee):
+    ...
+
+
+ 
+class ModelRoute(megrok.traject.Traject):
+    grok.context(Model)
+    traject.pattern('foo/:bar/clusters/:baz')
+
+    def factory(bar, baz):
+        return session.query(...).first()
+
+    def arguments(obj):
+        return {'bar': obj.zorgverzekeraar.id, 'baz': obj.id}
+



More information about the checkins mailing list