--- 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