# HG changeset patch # User Tero Marttila # Date 1234134640 -7200 # Node ID 99c45fc13edc860b121c81bd965467436bda6a7b # Parent 54c5f5f340debcc65217ba1d8accb3aa422c18a6 improved URLTree types (URLType), helpers.escape diff -r 54c5f5f340de -r 99c45fc13edc helpers.py --- a/helpers.py Sun Feb 08 03:17:07 2009 +0200 +++ b/helpers.py Mon Feb 09 01:10:40 2009 +0200 @@ -2,7 +2,7 @@ Helper functions for use in templates """ -import time +import time, cgi from qmsk.web import config @@ -40,3 +40,10 @@ ) for page in trail ) +def escape (data) : + """ + Escape data as HTML + """ + + return cgi.escape(data) + diff -r 54c5f5f340de -r 99c45fc13edc urltree.py --- 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[a-zA-Z_][a-zA-Z0-9_]*)(:(?P[a-zA-Z_][a-zA-Z0-9_]*))?(=(?P[^}]+))?\}$') - 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