[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