[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