urltree.py
changeset 47 99c45fc13edc
parent 46 54c5f5f340de
child 48 480adab03749
--- a/urltree.py	Sun Feb 08 03:17:07 2009 +0200
+++ b/urltree.py	Mon Feb 09 01:10:40 2009 +0200
@@ -39,9 +39,9 @@
     """
 
     @staticmethod
-    def parse (mask, defaults, types) :
+    def parse (mask, defaults, config) :
         """
-            Parse the given label-segment, and return a *Label instance
+            Parse the given label-segment, and return a *Label instance. Config is the URLConfig to use
         """
 
         # empty?
@@ -58,8 +58,8 @@
             # type
             type = match.group("type")
             
-            # lookup type, None for default
-            type = types[type]
+            # lookup type, None -> default
+            type = config.get_type(type)
 
             # defaults?
             default = defaults.get(key)
@@ -69,7 +69,7 @@
 
                 if default :
                     # apply type to default
-                    default = type(default)
+                    default = type.parse(default)
 
             # build
             return SimpleValueLabel(key, type, default)
@@ -215,7 +215,7 @@
 
     EXPR = re.compile(r'^\{(?P<key>[a-zA-Z_][a-zA-Z0-9_]*)(:(?P<type>[a-zA-Z_][a-zA-Z0-9_]*))?(=(?P<default>[^}]+))?\}$')
 
-    def __init__ (self, key, type=str, default=None) :
+    def __init__ (self, key, type, default) :
         """
             The given key is the name of this label's value
         """
@@ -238,44 +238,170 @@
         
         # only non-empty values!
         elif value :
+            # test
+            if not self.type.test(value) :
+                return False
+
             # convert with type
-            try :
-                value = self.type(value)
-
-            except Exception, e :
-                raise URLError("Bad value %r for type %s: %s: %s" % (value, self.type.__name__, type(e).__name__, e))
+            value = self.type.parse(value)
 
             return LabelValue(self, value)
 
     def __str__ (self) :
         return '{%s%s%s}' % (
             self.key, 
-            ':%s' % (self.type.__name__ ) if self.type != str else '',
+            ':%s' % (self.type, ),  # XXX: omit if default
             '=%s' % (self.default, ) if self.default else '',
         )
 
+class URLType (object) :
+    """
+        Handles the type-ness of values in the URL
+    """
+
+    def _init_name (self, name) :
+        """
+            Initialize our .name attribute, called by URLConfig
+        """
+
+        self.name = name
+
+    def test (self, value) :
+        """
+            Tests if the given value is valid for this type.
+
+            Defaults to calling parse(), and returning False on errors, True otherwise
+        """
+        
+        try :
+            self.parse(value)
+
+        except :
+            return False
+
+        else :
+            return True
+    
+    def parse (self, value) :
+        """
+            Parse the given value, which was tested earlier with test(), and return the value object
+        """
+
+        abstract
+    
+   
+    def build (self, obj) :
+        """
+            Reverse of parse(), return an url-value built from the given object
+        """
+
+        abstract
+
+    def __str__ (self) :
+        """
+            Return a short string giving the name of this type, defaults to self.name
+        """
+
+        return self.name
+ 
+class URLStringType (URLType) :
+    """
+        The default URLType, just plain strings.
+
+        Note that this does not accept empty strings as valid
+    """
+
+    def __init__ (self) :
+        super(URLStringType, self).__init__('str')
+
+    def parse (self, value) :
+        """
+            Identitiy
+        """
+
+        return value
+
+    def build (self, obj) :
+        if not obj :
+            raise ValueError("String must not be empty")
+
+        return str(obj)
+
+class URLIntegerType (URLType) :
+    """
+        A URLType for simple integers
+    """
+
+    def __init__ (self, negative=True, zero=True, max=None) :
+        """
+            Pass in negative=False to disallow negative numbers, zero=False to disallow zero, or non-zero max
+            to specifiy maximum value
+        """
+
+        super(URLIntegerType, self).__init__('int')
+
+        self.negative = negative
+        self.zero = zero
+        self.max = max
+    
+    def _validate (self, value) :
+        """
+            Test to make sure value fits our criteria
+        """
+
+        # negative?
+        if self.negative and value < 0 :
+            raise ValueError("value is negative")
+        
+        # zero?
+        if self.zero and value == 0 :
+            raise ValueError("value is zero")
+        
+        # max?
+        if self.max is not None and value > max :
+            raise ValueError("value is too large: %d" % value)
+        
+        return value
+
+    def parse (self, value) :
+        """
+            Convert str -> int
+        """
+
+        return self._validate(int(value))
+    
+    def build (self, obj) :
+        """
+            Convert int -> str
+        """
+
+        return unicode(self._validate(obj))
+    
 class URLConfig (object) :
     """
-        Global configuration relevant to all URLs
+        Global configuration relevant to all URLs. This can be used to construct a set of URLs and then create an
+        URLTree out of them. Simply call the url_config() instace with the normal URL arguments (except, of course,
+        config), and finally just pass the url_config to URLTree (it's iterable).
     """
 
     # built-in type codes
     BUILTIN_TYPES = {
-        # default
-        None    : str,
-
         # string
-        'str'   : str,
+        'str'   : URLStringType(),
 
         # integer
-        'int'   : int,
+        'int'   : URLIntegerType(),
     }
 
-    def __init__ (self, type_dict=None) :
+    # init names
+    for name, type in BUILTIN_TYPES.iteritems() :
+        type._init_name(name)
+
+    def __init__ (self, type_dict=None, default_type='str') :
         """
             Create an URLConfig for use with URL
 
-            If type_dict is given, it should be a mapping of type names -> callables, and they will be available for
+            If type_dict is given, it should be a dict of { type_names: URLType }, and they will be available for
             type specifications in addition to the defaults.
         """
 
@@ -284,15 +410,57 @@
         
         # apply the given type_dict
         if type_dict :
+            # initialize names
+            for name, type in type_dict.iteritems() :
+                type._init_name(name)
+            
+            # merge
             self.type_dict.update(type_dict)
 
+        # init
+        self.default_type = default_type
+        self.urls = []
+        
+    def get_type (self, type_name=None) :
+        """
+            Lookup an URLType by type_name, None for default
+        """
+        
+        # default type?
+        if not type_name :
+            type_name = self.default_type
+        
+        # lookup + return
+        return self.type_dict[type_name]
+
+    def __call__ (self, *args, **kwargs) :
+        """
+            Return new URL object with this config and the given args, adding it to our list of urls
+        """
+        
+        # build
+        url = URL(self, *args, **kwargs)
+        
+        # store
+        self.urls.append(url)
+
+        # return
+        return url
+    
+    def __iter__ (self) :
+        """
+            Returns all defined URLs
+        """
+
+        return iter(self.urls)
+
 class URL (object) :
     """
         Represents a specific URL
     """
 
 
-    def __init__ (self, config, url_mask, handler, type_dict=None, **defaults) :
+    def __init__ (self, config, url_mask, handler, **defaults) :
         """
             Create an URL using the given URLConfig, with the given url mask, handler, and default values.
         """
@@ -315,7 +483,7 @@
             query_mask = None
 
         # build our label path
-        self.label_path = [Label.parse(mask, defaults, config.type_dict) for mask in url_mask.split('/')]
+        self.label_path = [Label.parse(mask, defaults, config) for mask in url_mask.split('/')]
 
         # build our query args list
         if query_mask :
@@ -338,10 +506,10 @@
                 key = query_item
 
                 # type
-                type = self.config.type_dict[type]
+                type = self.config.get_type(type)
 
                 # add to query_args as (type, default) tuple
-                self.query_args[key] = (type, type(default) if default else default)
+                self.query_args[key] = (type, type.parse(default) if default else default)
          
     def get_label_path (self) :
         """
@@ -373,8 +541,8 @@
                 value = None
 
             else :
-                # process value
-                value = type(value)
+                # parse value
+                value = type.parse(value)
 
             # set default?
             if not value :
@@ -639,6 +807,8 @@
             Find the URL object best corresponding to the given url, matching any ValueLabels.
 
             Returns an (URL, [LabelValue]) tuple.
+
+            XXX: handle unicode on URLs
         """
 
         # split it into labels