[Checkins] SVN: traject/trunk/ Add converter support.
Martijn Faassen
faassen at startifact.com
Thu Nov 26 08:22:05 EST 2009
Log message for revision 106022:
Add converter support.
Changed:
U traject/trunk/CHANGES.txt
U traject/trunk/src/traject/__init__.py
U traject/trunk/src/traject/_traject.py
U traject/trunk/src/traject/tests.py
U traject/trunk/src/traject/traject.txt
-=-
Modified: traject/trunk/CHANGES.txt
===================================================================
--- traject/trunk/CHANGES.txt 2009-11-26 13:19:47 UTC (rev 106021)
+++ traject/trunk/CHANGES.txt 2009-11-26 13:22:04 UTC (rev 106022)
@@ -4,11 +4,18 @@
0.10 (unreleased)
=================
-- Nothing changed yet.
+- Add converter functionality for path segments. This makes it
+ possible to ensure that a path segment is of a certain type, such as
+ an integer, using ``a/:id:int``.
+ Converters ``unicode``, ``str``, ``int``, ```unicodelist``,
+ `strlist`` and ``intlist`` are known. ``unicode`` is the default
+ converter used if no converter was specified. New converters can be
+ registered using ``register_converter``. If a converter raises a
+ ValueError, the path cannot be resolved.
0.9 (2009-11-16)
================
-* Initial public release.
+- Initial public release.
Modified: traject/trunk/src/traject/__init__.py
===================================================================
--- traject/trunk/src/traject/__init__.py 2009-11-26 13:19:47 UTC (rev 106021)
+++ traject/trunk/src/traject/__init__.py 2009-11-26 13:22:04 UTC (rev 106022)
@@ -1,5 +1,5 @@
from traject._traject import parse, subpatterns, Patterns
-from traject._traject import (register, register_inverse,
+from traject._traject import (register_converter, register, register_inverse,
resolve, resolve_stack,
consume, consume_stack,
locate)
Modified: traject/trunk/src/traject/_traject.py
===================================================================
--- traject/trunk/src/traject/_traject.py 2009-11-26 13:19:47 UTC (rev 106021)
+++ traject/trunk/src/traject/_traject.py 2009-11-26 13:22:04 UTC (rev 106022)
@@ -96,7 +96,26 @@
def __init__(self):
self._registry = AdapterRegistry()
self._inverse_registry = AdapterRegistry()
+ self._converters = {
+ 'unicode': convert_unicode,
+ 'str': convert_str,
+ 'int': int,
+ 'unicodelist': convert_unicodelist,
+ 'strlist': convert_strlist,
+ 'intlist': convert_intlist,
+ }
+
+ def register_converter(self, converter_name, converter_func):
+ """Register a converter function for a converter name.
+
+ Type names are the optional bit in a pattern, behind
+ a second colon (i.e. :foo_id:int).
+ "A converter function must raise ValueError if it cannot
+ convert to the desired format.
+ """
+ self._converters[converter_name] = converter_func
+
def register(self, class_or_interface, pattern_str, factory):
interface = _get_interface(class_or_interface)
pattern = parse(pattern_str)
@@ -105,6 +124,13 @@
name = component_name(p)
if name[-1] == ':':
value = p[-1][1:]
+ if ':' in value:
+ dummy, converter_name = value.split(':')
+ if converter_name not in self._converters:
+ raise RegistrationError(
+ "Could not register %s because no converter "
+ "can be found for variable %s" %
+ ('/'.join(pattern), value))
prev_value = self._registry.registered(
(interface,), IStep, name)
if prev_value == value:
@@ -178,8 +204,20 @@
# if so, we matched the variable pattern
pattern = variable_pattern
pattern_str = variable_pattern_str
+ # we parse the variable to see whether the name
+ # fits what we expect
+ if ':' in variable:
+ variable, converter_name = variable.split(':')
+ else:
+ converter_name = 'unicode'
+ converter = self._converters[converter_name]
+ try:
+ converted = converter(name)
+ except ValueError:
+ stack.append(name)
+ return stack, consumed, obj
# the variable name is registered
- variables[str(variable)] = name
+ variables[str(variable)] = converted
else:
# cannot find step or variable, so cannot resolve
stack.append(name)
@@ -244,12 +282,34 @@
# we're done, as parent as a parent itself
return
+def convert_unicodelist(s):
+ return [convert_unicode(v) for v in s.split(';')]
+
+def convert_strlist(s):
+ return [convert_str(v) for v in s.split(';')]
+
+def convert_intlist(s):
+ return [int(v) for v in s.split(';')]
+
+def convert_unicode(s):
+ try:
+ return unicode(s)
+ except UnicodeError:
+ raise ValueError
+
+def convert_str(s):
+ try:
+ return str(s)
+ except UnicodeError:
+ raise ValueError
+
def normalize(pattern_str):
if pattern_str.startswith('/'):
return pattern_str[1:]
return pattern_str
_patterns = Patterns()
+register_converter = _patterns.register_converter
register = _patterns.register
register_inverse = _patterns.register_inverse
resolve = _patterns.resolve
Modified: traject/trunk/src/traject/tests.py
===================================================================
--- traject/trunk/src/traject/tests.py 2009-11-26 13:19:47 UTC (rev 106021)
+++ traject/trunk/src/traject/tests.py 2009-11-26 13:22:04 UTC (rev 106022)
@@ -529,6 +529,21 @@
patterns.register,
Root, 'departments/:other_id/employees/:employee_id', Employee)
+ def test_conflicting_converters(self):
+ patterns = traject.Patterns()
+ patterns.register(
+ Root, 'departments/:department_id',
+ Department)
+ self.assertRaises(
+ traject.RegistrationError,
+ patterns.register,
+ Root, 'departments/:department_id:int', Department)
+ self.assertRaises(
+ traject.RegistrationError,
+ patterns.register,
+ Root, 'departments/:department_id:int/employees/:employee_id',
+ Employee)
+
def test_override_variable_names(self):
patterns = traject.Patterns()
patterns.register(
@@ -622,8 +637,96 @@
self.assert_(isinstance(obj, Obj))
self.assertEquals('B', obj.b)
self.assertEquals('D', obj.d)
-
+ def test_match_int(self):
+ patterns = traject.Patterns()
+ class Obj(object):
+ def __init__(self, v):
+ self.v = v
+ patterns.register(Root, 'a/:v:int', Obj)
+ root = Root()
+ obj = patterns.resolve(root, 'a/1', default)
+ self.assertEquals(1, obj.v)
+
+ def test_mismatch_int(self):
+ patterns = traject.Patterns()
+ class Obj(object):
+ def __init__(self, v):
+ self.v = v
+ patterns.register(Root, 'a/:v:int', Obj)
+ root = Root()
+ self.assertRaises(traject.ResolutionError,
+ patterns.resolve, root, 'a/b', default)
+
+ def test_match_strlist(self):
+ patterns = traject.Patterns()
+ class Obj(object):
+ def __init__(self, v):
+ self.v = v
+ patterns.register(Root, 'a/:v:strlist', Obj)
+ root = Root()
+ obj = patterns.resolve(root, 'a/a;b;c', default)
+ self.assertEquals(['a', 'b', 'c'], obj.v)
+ obj = patterns.resolve(root, 'a/b', default)
+ self.assertEquals(['b'], obj.v)
+
+ def test_match_intlist(self):
+ patterns = traject.Patterns()
+ class Obj(object):
+ def __init__(self, v):
+ self.v = v
+ patterns.register(Root, 'a/:v:intlist', Obj)
+ root = Root()
+ obj = patterns.resolve(root, 'a/1;2;3', default)
+ self.assertEquals([1, 2, 3], obj.v)
+ obj = patterns.resolve(root, 'a/1', default)
+ self.assertEquals([1], obj.v)
+
+ def test_mismatch_intlist(self):
+ patterns = traject.Patterns()
+ class Obj(object):
+ def __init__(self, v):
+ self.v = v
+ patterns.register(Root, 'a/:v:intlist', Obj)
+ root = Root()
+ self.assertRaises(traject.ResolutionError,
+ patterns.resolve, root, 'a/a;b', default)
+
+ def test_consume_mismatch_int(self):
+ patterns = traject.Patterns()
+ class Obj(object):
+ def __init__(self, v):
+ self.v = v
+ patterns.register(Root, 'a/:v:int', Obj)
+ root = Root()
+ unconsumed, consumed, obj = patterns.consume(root,
+ 'a/not_an_int',
+ default)
+ self.assertEquals(['not_an_int'], unconsumed)
+ self.assertEquals(['a'], consumed)
+ self.assertEquals('a', obj.__name__)
+ self.assert_(obj.__parent__ is root)
+
+ def test_unknown_converter(self):
+ patterns = traject.Patterns()
+ class Obj(object):
+ def __init__(self, v):
+ self.v = v
+ self.assertRaises(traject.RegistrationError,
+ patterns.register, Root, 'a/:v:foo', Obj)
+
+ def test_new_converter(self):
+ patterns = traject.Patterns()
+ patterns.register_converter('float', float)
+ class Obj(object):
+ def __init__(self, v):
+ self.v = v
+ patterns.register(Root, 'a/:v:float', Obj)
+
+ root = Root()
+ obj = patterns.resolve(root, 'a/1.1', default)
+ self.assertEquals(1.1, obj.v)
+
# XXX need a test for trailing slash?
# XXX could already introspect function to see whether we can properly
@@ -858,6 +961,7 @@
patterns.locate(root, employee, default)
self.assertEquals([u'employee 1 2', u'employees 1'],
_calls)
+
def test_inverse_non_unicode_name(self):
patterns = self.get_identity_patterns()
Modified: traject/trunk/src/traject/traject.txt
===================================================================
--- traject/trunk/src/traject/traject.txt 2009-11-26 13:19:47 UTC (rev 106021)
+++ traject/trunk/src/traject/traject.txt 2009-11-26 13:22:04 UTC (rev 106022)
@@ -186,7 +186,9 @@
The factory function in this case just creates a ``Employee`` object
on the fly. In the context of a relation database it could instead
-perform a database query based on the parameters supplied.
+perform a database query based on the parameters supplied. If the
+factory returns ``None``, this is interpreted as the system being
+unable to match the URL: the object cannot be found.
In order to register this factory function, we need a registry of
patterns, so we'll create one::
@@ -246,6 +248,10 @@
>>> obj = patterns.resolve(root, u'departments/1/employees/2', Default)
>>> obj
<Employee 1 2>
+ >>> obj.department_id
+ u'1'
+ >>> obj.employee_id
+ u'2'
An alternative ``resolve_stack`` method allows us to resolve a stack
of names instead (where the first name to resolve is on the top of the
@@ -255,7 +261,61 @@
>>> l.reverse()
>>> patterns.resolve_stack(root, l, Default)
<Employee 1 2>
+
+Converters
+==========
+
+It is possible to specify converters in patterns. A converter is a
+function that converts a value to a desired value, and raises a
+ValueError if this is not possible. The build-in ``int`` in Python is
+an example of a converter.
+A converter is specified in a pattern with an extra colon and then a
+converter identifier (``int`` in this case)::
+
+ >>> pattern_str = u'individuals/:individual_id:int'
+
+Traject comes with a number of built-in converters:
+
+* ``unicode``: the default converter. Tries to convert the input to
+ a unicode value. If no converter is specified, it will use this.
+
+* ``str``: tries to convert the input to a string.
+
+* ``int``: tries to convert the input to an integer.
+
+* ``unicodelist``: tries to convert the input to a list of unicode
+ strings. The input is split on the ``;`` character.
+
+* ``strlist``: tries to convert the input to a list of strings. The
+ input is split on the ``;`` character.
+
+* ``intlist``: tries to convert the input to a list of integers. The
+ input is split on the ``;`` character.
+
+We now register the pattern::
+
+ >>> class Individual(object):
+ ... def __init__(self, individual_id):
+ ... self.individual_id = individual_id
+ ... def __repr__(self):
+ ... return '<Individual %s>' % self.individual_id
+ >>> patterns.register(Root, pattern_str, Individual)
+
+ >>> indiv = patterns.resolve(root, u'individuals/1', Default)
+
+We see that the value has indeed been converted to an integer::
+
+ >>> indiv.individual_id
+ 1
+
+New converters can be registered using the ``register_converter``
+method. This method takes two arguments: the converter name, and the
+converter function. The converter function should take a single
+argument and convert it to the desired value. If conversion fails, a
+``ValueError`` should be raised. The Python ``int`` function is an
+example of a valid converter.
+
Locations
=========
@@ -437,6 +497,7 @@
traject.register
traject.register_inverse
+ traject.register_converter
traject.resolve
traject.resolve_stack
traject.consume
More information about the checkins
mailing list