# HG changeset patch # User Tero Marttila # Date 1294581148 -7200 # Node ID e3001377e9dcf69754510ca8b118f17f2469e936 # Parent 30af52a271a179d33c90626a3d960db11c1f17a7 ext.urltree: improved docs and refactoring diff -r 30af52a271a1 -r e3001377e9dc svv/ext/urltree.py --- a/svv/ext/urltree.py Sun Jan 09 00:50:21 2011 +0200 +++ b/svv/ext/urltree.py Sun Jan 09 15:52:28 2011 +0200 @@ -1,5 +1,23 @@ """ - Tree-based URL mapping + Tree-based URL mapping. + + urltree is used to construct a tree-like structure of URL handlers, and to match a given URL against the tree, + returning the matching handler. + + URLs are handled as a sequence of labels, separated by slashes (/). Thus, an URL like /foo/bar/baz would be handled + as a list of three labels: + ['foo', 'bar', 'baz'] + + The URL handler rules are constructed as a sequence of Labels, which then match and evaluate URL segments. + + StaticLabel just matches a pre-defined/constant segment, and is used to route URLs through the URL tree. + + ValueLabel matches segments dynamically, capturing them as a typed value. It also supports default values. + + urltree also handles building URL strings for a given handler, and a set of values. + + urltree can also handle query-string parameters (i.e. GET params). + """ import re @@ -9,109 +27,104 @@ class URLError (Exception) : """ - Error with an URL definition + Error with an URL definition, parsing, or value handling. """ pass -class LabelValue (object) : +class URLRuleError (URLError) : """ - Represents the value of a ValueLabel... love these names + Error in URLTree structure definition. """ - def __init__ (self, label, value, is_default) : - """ - Just store - """ +class URLMatchError (URLError) : + """ + Error looking up the URL against our URLTree. + """ - self.label = label - self.value = value - self.is_default = is_default - - def __str__ (self) : - return "%s%s" % (self.label.key, "=%r" % (self.value, ) if not self.is_default else '') +class URLValueError (URLError) : + """ + Error parsing an URL segment's value via URLType. + """ - def __repr__ (self) : - return "<%s>" % self + pass + +class URLBuildError (URLError) : + """ + Error re-constructing an outgoing URL. + """ class Label (object) : """ - Base class for URL labels (i.e. the segments of the URL between /s) + Abstract base class for URL segment-matching rules. + + A Label subclass is constructed by parse()ing a text-form URL segment spec (== label), and knows how to do a + yes/value/no match() against an URL segment, as well as re-build() the URL segment for the matched value. + + XXX: rename to URLRuleLabel or something like that """ @staticmethod - def parse (mask, defaults, config) : + def parse (spec, defaults, config) : """ - Parse the given label-segment, and return a *Label instance. Config is the URLConfig to use + Parse the given label-spec, and return an appropriate Label subclass instance. + + spec - the rule specification as text + defaults - externally supplied { key: default } dict of default values + config - URLConfig instance to use for constructed Label """ - # empty? - if not mask : + # empty/root? + if not spec : return EmptyLabel() - - # simple value? - match = SimpleValueLabel.EXPR.match(mask) - - if match : - # key - key = match.group('key') - - # type - type_name = match.group("type") - - # lookup type, None -> default - type = config.get_type(type_name) + + # construct subclass instance by matching against regexps + for label_type in ( + ValueLabel, + StaticLabel, + ) : + # test + match = label_type.PARSE_RE.match(spec) - # defaults? - default = defaults.get(key) - - if not default : - default = match.group('default') - - if default : - # apply type to default - default = type.parse(default) - - # build - return SimpleValueLabel(key, type_name, type, default) - - # static? - match = StaticLabel.EXPR.match(mask) - - if match : - return StaticLabel(match.group('name')) + if match : + return label_type.parse(match, defaults, config) # invalid - raise URLError("Invalid label: %r" % (mask, )) + raise URLError("Unrecognized label: %r" % (spec, )) def match (self, value=None) : """ - Match this label against the given value, returning either True to match without a value, a LabelValue - object, or boolean false to not match. + Match this label against the given URL segment, returning one of : + + * True to match without a value + * False/None to not match, and continue looking + * a (key, value, label, is_default) capture-tuple to capture a named value from the URL segment - If value is None, this means that only a default value should be returned. + value - a segment from the URL as text, or None to force default value + """ + + raise NotImplemented() + + def build (self, values) : + """ + Re-construct this label, using values from the supplied { key: value } dict if needed. """ - abstract - - def build (self, value_dict) : + raise NotImplemented() + + def build_default (self, values) : """ - Return a string representing this label, using the values in the given value_dict if needed + Return an (is_default, value) tuple + + XXX: used internally by build()? """ - abstract - - def build_default (self, value_dict) : - """ - Return an (is_default, value) tuple - """ - - abstract + raise NotImplemented() class EmptyLabel (Label) : """ - An empty label, i.e. just a slash in the URL + An empty label, which matches empty URL segments, i.e. '//foo' or '/' """ def __eq__ (self, other) : @@ -123,7 +136,7 @@ def match (self, value=None) : """ - Match empty string -> True + Matches empty segment. """ # no default @@ -133,6 +146,8 @@ # only empty segments if value == '' : return True + + return False def build (self, values) : return '' @@ -145,10 +160,19 @@ class StaticLabel (Label) : """ - A simple literal Label, used for fixed terms in the URL + A simple literal Label, used to match fixed segments in the URL. """ - EXPR = re.compile(r'^(?P[a-zA-Z0-9_.-]+)$') + PARSE_RE = re.compile(r'^(?P[a-zA-Z0-9_.-]+)$') + + @classmethod + def parse (cls, match, defaults, config) : + """ + Construct and return a new instance from the given PARSE_RE match + """ + + # use name as required segment value + return cls(match.group('name')) def __init__ (self, name) : """ @@ -174,9 +198,14 @@ return False # match name - if value == self.name : + elif value == self.name : return True + # fail + else : + return False + + def build (self, values) : return self.name @@ -188,18 +217,63 @@ class ValueLabel (Label) : """ - A label with a key and a value - - XXX: do we even need this? + A label with a key and a typed value """ + + # / "{" [ ":" ] [ "=" [ ] ] "}" / + PARSE_RE = re.compile(r'^\{(?P[a-zA-Z_][a-zA-Z0-9_]*)(:(?P[a-zA-Z_][a-zA-Z0-9_]*))?(=(?P[^}]*))?\}$') - def __init__ (self, key, default) : + @classmethod + def parse (cls, match, defaults, config) : """ - Set the key and default value. Default value may be None if there is no default value defined + Construct and return a new instance from the given PARSE_RE match + """ + + # value name + key = match.group('key') + + # value type, or None + type_name = match.group("type") + + # lookup URLType, None -> default type + type = config.get_type(type_name) + + # get default value from dict, or None + default = defaults.get(key) + + if not default : + # default vlaue from expr? + default = match.group('default') + + if default : + # parse text-form default to actual value + # XXX: is this "" or None if match is for '{foo=}' ? + default = type.parse(default) + + # build + return cls(key, type, default, type_name) + + + def __init__ (self, key, type, default, type_name) : + """ + The given key is the name of this label's value. + + The given type_name is is None for the default type, otherwise the type's name. Type is a URLType. + + + key - the name of this label's value + type - the URLType used to parse/build the value + default - default for value, if no value is given in URL, or None for no default (value required) + type_name - parsed name for the type; used for debugging """ + # store self.key = key self.default = default + + # type + self.type_name = type_name + self.type = type def __eq__ (self, other) : """ @@ -208,6 +282,30 @@ return isinstance(other, ValueLabel) and self.key == other.key + def match (self, seg=None) : + """ + Match segment -> (key, value, type, is_default) + """ + + if seg is None and self.default is not None : + # default value + return self.key, self.default, self.type, True + + # we don't have a default value, so don't match partial URLs + elif not seg : + return False + + # test against type's syntax + elif not self.type.test(seg) : + return False + + else : + # convert with type + value = self.type.parse(seg) + + # False == non-default value + return self.key, value, self.type, False + def build (self, values) : """ Return either the assigned value from values, our default value, or raise an error @@ -218,7 +316,7 @@ def build_default (self, values) : """ - Check if we have a value in values, and return based on that + Check if we have a value in values, and return based on that. """ # state @@ -226,9 +324,9 @@ # value given? if self.key not in values or values[self.key] is None : - # error on missing non-default if self.default is None : - raise URLError("No value given for label %r" % (self.key, )) + # no value given for required label + raise URLBuildError("No value given for label %r" % (self.key, )) # use default else : @@ -236,67 +334,164 @@ value = self.default else : - # lookup the value obj to use + # lookup the value to use value = values[self.key] - # default? + # smart-match against default value + # XXX: would it be better not to omit it as a default value in this case? is_default = bool(value == self.default) - # convert value back to str + # convert value to str value = self.type.build(value) # return return (is_default, value) -class SimpleValueLabel (ValueLabel) : - """ - A label that has a name and a simple string value - """ - - 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_name, type, default) : + def __str__ (self) : """ - The given key is the name of this label's value. - - The given type_name is is None for the default type, otherwise the type's name. Type is a URLType. + Pretty-format back into parsed form. """ - # type - self.type_name = type_name - self.type = type - - # store - self.key = key - self.default = default - - def match (self, value=None) : - """ - Match -> LabelValue - """ - - # default? - if value is None and self.default is not None : - return LabelValue(self, self.default, True) - - # only non-empty values! - elif value : - # test - if not self.type.test(value) : - return False - - # convert with type - value = self.type.parse(value) - - return LabelValue(self, value, False) - - def __str__ (self) : return '{%s%s%s}' % ( self.key, (':%s' % (self.type_name, ) if self.type_name is not None else ''), '=%s' % (self.default, ) if self.default is not None else '', ) +class QueryItem (object) : + """ + Like a normal URL Label, except for dealing with query-string arguments. + """ + + @classmethod + def parse (cls, spec, defaults, config) : + """ + Construct and return a new instance from the given spec + """ + + # parse default value + if '=' in spec : + # default value, might be empty + spec, default_spec = spec.split('=') + + else : + # no default, value required + default_spec = None + + # parse type + if ':' in spec : + # use named type + spec, type_name = spec.split(':') + else : + # use default type + type_name = None + + # name for value + key = spec + + # URLType + type = config.get_type(type_name) + + if key in defaults : + # default value from external defaults dict + default = defaults[key] + + elif default_spec : + # given default value spec + default = type.parse(default_spec) + + elif default_spec is not None : + # XXX: no default value, but not required either... silly + default = '' + + else : + # no default value, required + default = None + + # build + return cls(key, type, default) + + def __init__ (self, key, type, default) : + """ + The given key is the name of this label's value. + + The given type_name is is None for the default type, otherwise the type's name. Type is a URLType. + + + key - the name of this label's value + type - the URLType used to parse/build the value + default - default for value, if no value is given in URL, or None for no default (value required) + """ + + # store + self.key = key + self.type = type + self.default = default + + def match (self, arg=None) : + """ + Parse value from given query-string argument for this key + + arg - the list of query-string values given, or None for missing value. + + Returns value to use, or None to omit. + """ + + if not arg : + if self.default is None : + # no default value! + raise URLValueError("No value given for query string param: ?%s=..." % (self.key, )) + + elif self.default == '' : + # XXX: omit + return None + + else : + # use default value + return self.default + + # parse with our type + value = None + + for value_spec in arg : + if value is None : + # parse first + value = self.type.parse(value_spec) + + else : + # parse multiple + value = self.type.append(value, self.type.parse(value_spec)) + + # ok + return value + + def build (self, values) : + """ + Build and return list of query-string values to pass for this argument, or None to omit. + """ + + if self.key not in values : + if self.default is None : + # fail + raise URLBuildError("No value given for query string arugment %r" % (self.key, )) + + else : + # omit, and use default value + return None + + else : + # lookup our value + value = values[self.key] + + if value is not None : + # map to values + return self.type.build_multi(value) + + else : + # omit + return None + + class URLType (object) : """ Handles the type-ness of values in the URL @@ -304,72 +499,79 @@ def test (self, value) : """ - Tests if the given value is valid for this type. + Tests if the given value is accepted for this type. Defaults to calling parse(), and returning False on errors, True otherwise """ try : self.parse(value) - - except : + + # XXX: change to URLValueError to not swallow bugs + except Exception : return False else : return True - def parse (self, value) : + def parse (self, seg) : """ - Parse the given value, which was tested earlier with test(), and return the value object + Parse the given URL segment text, which was tested earlier with test(), and return the value object. + + Raise an URLValueError to reject the value via the default test() implementation. """ - - abstract + + raise NotImplementedError() def append (self, old_value, value) : """ - Handle multiple values for this type, by combining the given old value and new value (both from parse). + Handle multi-valued values, by combining the given previously parse()'d value and newly parse()'d value. - Defaults to raise an error + Defaults to raise an error. + """ + + raise URLValueError("Multiple values given") + + def build (self, value) : + """ + Reverse of parse(), return an URL segment built from the given object value """ - raise URLError("Multiple values for argument") - - def build (self, obj) : - """ - Reverse of parse(), return an url-value built from the given object + raise NotImplementedError() + + def build_multi (self, value) : """ - - abstract - - def build_multi (self, obj) : - """ - Return a list of string values for the given object value (as from parse/append). + Return a list of URL segments for the given object value (as from parse/append), for multi-value types. Defaults to return [self.build(obj)] """ - return [self.build(obj)] + return [self.build(value)] class URLStringType (URLType) : """ The default URLType, just plain strings. - XXX: decodeing here, or elsewhere? + XXX: handle unicode here, or assume URL is unicode? """ - def parse (self, value) : + def parse (self, seg) : """ Identitiy """ - return value + return unicode(seg) - def build (self, obj) : - return str(obj) + def build (self, value) : + """ + Return value's string representation for URL + """ + + return unicode(value) class URLIntegerType (URLType) : """ - A URLType for simple integers + A URLType for simple int's, with some constraing checking """ def __init__ (self, allow_negative=True, allow_zero=True, max=None) : @@ -389,24 +591,32 @@ # negative? if not self.allow_negative and value < 0 : - raise ValueError("value is negative") + raise URLValueError("value is negative") # zero? if not self.allow_zero and value == 0 : - raise ValueError("value is zero") + raise URLValueError("value is zero") # max? if self.max is not None and value > self.max : - raise ValueError("value is too large: %d" % value) + raise URLValueError("value is too large: %d" % value) return value - def parse (self, value) : + def parse (self, seg) : """ Convert str -> int """ - return self._validate(int(value)) + try : + value = int(seg) + + except ValueError, ex : + # reject + raise URLValueError(ex) + + # validate + return self._validate(value) def build (self, obj) : """ @@ -415,27 +625,41 @@ return unicode(self._validate(obj)) -class URLListType (URLType) : +class URLListType (URLStringType) : """ A list of strings """ - def parse (self, value) : - return [value] + def parse (self, seg) : + """ + URL segments are just parsed into single-item lists. + """ + + return [super(URLListType, self).parse(seg)] def append (self, old_value, value) : + """ + Accumulate values in list. + """ + + # combine lists return old_value + value - def build_multi (self, obj) : - return obj + def build_multi (self, value) : + """ + Return as a list of build()'d values. + """ + + return [super(URLListType, self).build(item) for item in value] class URLConfig (object) : """ - 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). + Shared configuration relevant to a set of constructed URLRules. - XXX: rename to URLFactory? + This can be used as a factory to accumulate constructed URLRules, and then create an URLTree out of them. + + Simply __call__ the URLConfig() instance with the normal URLRule() *args (except, of course, config=), and finally + just pass the URLConfig() to URLTree() - it's iter()able. """ # built-in type codes @@ -453,32 +677,32 @@ 'list' : URLListType(), } - def __init__ (self, type_dict=None, ignore_extra_args=True) : + def __init__ (self, type_dict=None, reject_extra_args=None) : """ - Create an URLConfig for use with URL + Create an URLConfig for use with URLTree/URLs. - 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. This will call type._init_name with the key, so do - *not* initialize the name yourself. - - If ignore_extra_args is given, unrecognized query arguments will be ignored. + type_dict - (optional) { type_name: URLType } dict of additional URLTypes to make + available for use in URLRules. This will take care of ._init_name(). + Use None as a key to change the default URLType. + reject_extra_args - instead of ignoring unrecognized query arguments in matched URLs, reject them """ - # build our type_dict + # our type_dict self.type_dict = self.BUILTIN_TYPES.copy() - # apply the given type_dict if type_dict : # merge self.type_dict.update(type_dict) # init - self.ignore_extra_args = ignore_extra_args + self.ignore_extra_args = not(reject_extra_args) + + # accumulated URLRules self.urls = [] def get_type (self, type_name=None) : """ - Lookup an URLType by type_name, None for default. + Lookup an URLType by type_name, None for default type. """ # lookup + return @@ -486,11 +710,11 @@ def __call__ (self, *args, **kwargs) : """ - Return new URL object with this config and the given args, adding it to our list of urls + Return new URLRule with this config and the given args, adding it to our list of urls """ # build - url = URL(self, *args, **kwargs) + url = URLRule(self, *args, **kwargs) # store self.urls.append(url) @@ -500,182 +724,211 @@ def __iter__ (self) : """ - Returns all defined URLs + Returns all defined URLRules """ return iter(self.urls) -class URL (object) : +class URLRule (object) : """ - Represents a specific URL + A set of Labels defining a path to a handler in the URLTree, parsed from a string-form URL-spec. + + XXX: also handles query_args, spec that properly... """ - - def __init__ (self, config, url_mask, handler, **defaults) : + def __init__ (self, config, url_spec, handler, **defaults) : """ Create an URL using the given URLConfig, with the given url mask, handler, and default values. + + config - the URLConfig() used for this rule + url_spec - a string-form sequence of Label rules to parse + Should be rooted, i.e. start with a '/' + handler - the target of this URLRule(), returned as the result of the lookup + **defaults - additional default values. Can be used to specify/override the default values in the + url_spec for complex values, or just extra args to pass through. """ # store self.config = config - self.url_mask = url_mask + self.url_spec = url_spec self.handler = handler self.defaults = defaults - # query string - self.query_args = dict() + # query string spec + self.query_items = dict() # remove prepending root / - url_mask = url_mask.lstrip('/') + url_spec = url_spec.lstrip('/') # parse any query string # XXX: conflicts with regexp syntax - if '/?' in url_mask : - url_mask, query_mask = url_mask.split('/?') + if '/?' in url_spec : + url_spec, query_spec = url_spec.split('/?') else : - query_mask = None - - # build our label path - self.label_path = [Label.parse(mask, defaults, config) for mask in url_mask.split('/')] - - # build our query args list - if query_mask : - # split into items - for query_item in query_mask.split('&') : - # parse default - if '=' in query_item : - query_item, default = query_item.split('=') + query_spec = None - else : - default = None - - # parse type - if ':' in query_item : - query_item, type = query_item.split(':') - else : - type = None - - # parse key - key = query_item + # parse the spec into a sequence of Labels + self.label_path = [Label.parse(label_spec, defaults, config) for label_spec in url_spec.split('/')] - # type - type = self.config.get_type(type) + # parse query args + if query_spec : + for item_spec in query_spec.split('&') : + # parse spec into QueryItem + query_item = QueryItem.parse(item_spec, defaults, config) - # add to query_args as (type, default) tuple - self.query_args[key] = (type, type.parse(default) if default else default) + # store, case-insensitive + self.query_items[query_item.key.lower()] = query_item def get_label_path (self) : """ - Returns a list containing the labels in this url + Returns the Label-path for this URLRule. """ # copy self.label_path return list(self.label_path) - def execute (self, request, label_values) : - """ - Invoke the handler, using the given label values + def evaluate (self, url_values, query_args) : """ - - # start with the defaults - kwargs = self.defaults.copy() - - # ...dict of those label values which are set to defaults - default_labels = {} - - # then add all the values - for label_value in label_values : - kwargs[label_value.label.key] = label_value.value + Process the set of argument matched against this URLRule, both those values captured from the URL by + Labels, and any incoming query string arguments: - # add key to default_values? - if label_value.is_default : - default_labels[label_value.label.key] = label_value.label - - # then parse all query args - for key, value in request.get_args() : - # lookup in our defined query args - if key in self.query_args : - # lookup spec - type, default = self.query_args[key] + url_values - a [ (key, value, type, is_default) ] sequence of capture-tuples values from URL segments/labels + query_args - a [ (key, [ value ] ] sequence of query string arguments. This will be mutated! - # override URL params if they were not given - elif key in default_labels : - type, default = default_labels[key].type, None - - # be strict about extraneous query args? - elif not self.config.ignore_extra_args : - raise URLError("Unrecognized query argument: %r" % (key, )) - - # ignore + Returns the parsed set of arguments, and the remaining query string arguments: + + return ( + args - a { key: value } dict of label and query string values, suitable for passing to + handler function as keyword arguments + qargs - a [ (key, [ value ] ] sequence of remaining query-string arguments not included + in kwargs + """ + + # convert url_values to dict for lookup + url_values = dict((key, (value, type, is_default)) for key, value, type, is_default in url_values) + + # collect a list of (key, value, is_default) tuples to later apply + values = [] + + # start with the explicitly supplied defaults + # these may actually include additional arguments to be passed through to the handler + for key, value in self.defaults.iteritems() : + values.append((key, value, True)) + + # then add all the label values matched from the URL + for key, (value, type, is_default) in url_values.iteritems() : + values.append((key, value, is_default)) + + # then add in query args + for key, value_list in query_args : + # case-insensitive + key = key.lower() + + if key in self.query_items : + # defined as a QueryItem + query_item = self.query_items[key] + + # parse as list/value + value = query_item.match(value_list) + + # override URL label values if they were parsed as implicit defaults + elif key in url_values : + old_value, type, is_default = url_values[key] + + if not is_default : + # XXX: ignore extra value as per original implementation + continue + + # parse into value + value = None + + for value_spec in value_list : + if value is None : + # parse + value = type.parse(value_spec) + + else : + # multi-value + # XXX: this will usually fail, we can't really mix these very well + value = type.append(value, type.parse(value_spec)) + + # unknown, ignore else : continue - # normalize empty value to None - if not value : - value = None - - else : - # parse value - value = type.parse(value) - - # XXX: this should set it to True or something... it's a flag, "/foo?bar" - # set default? - if value is None : - if default == '' : - # do not pass key at all - continue + # apply + values.append((key, value, False)) + + # outgoing args + args = {} + + # args set to default value + default_args = set() - elif default is not None : - # pass default value - value = default + # apply values + for key, value, is_default in values : + # fresh value + if key not in args : + # store + args[key] = value - else : - # required arg, no default - raise URLError("No value given for required argument: %r" % (key, )) + if is_default : + default_args.add(key) - # already have a non-default value? - if key in kwargs and key not in default_labels : - # append to old value - kwargs[key] = type.append(kwargs[key], value) + # default value + elif key in args and key in default_args : + # replace default value with more specific value + args[key] = value + if not is_default : + default_args.remove(key) + + # existing value else : - # set key - kwargs[key] = value - - # then check all query args - for key, (type, default) in self.query_args.iteritems() : - # skip those already present - if key in kwargs : + raise URLValueError("Multiple values given for param: %s" % (key, )) + + # then check for missing query_items + for key, query_item in self.query_items.iteritems() : + # skip those already parsed + if key in args : continue - # apply default? - if default is None : - raise URLError("Missing required argument: %r" % (key, )) + # apply default + value = query_item.match(None) - elif default == '' : - # skip empty default - continue + if value is not None : + # set + args[key] = value - else : - # set default - kwargs[key] = default + # remaining qargs not handled + qargs = [(key, values) for key, values in query_args if key not in args] - # execute the handler - return self.handler(request, **kwargs) + # ok + return args, qargs - def build (self, request, **values) : + def build (self, **values) : """ - Build an absolute URL pointing to this target, with the given values. Default values are left off if they - are at the end of the URL. + Build an absolute URL from our URLTree's root pointing to this target, with the given values embedded. + Default values are left off if they are at the end of the URL. Values given as None are ignored. + + Returns ( + * the absolute URL from our URLTree's root to this URLRule as a str + * query args for URL as a [ (key, [value]) ] list + ) + + # XXX: reject unrecognized values """ - # collect segments as a list of (is_default, segment) values - segments = [(False, request.page_prefix)] + [label.build_default(values) for label in self.label_path] + # root + segments = [(False, '')] - # trim default items off the end + # collect our Label-path's with values as a list of (is_default, segment) tuples + segments += [label.build_default(values) for label in self.label_path] + + # optimize by trimming None/default items off the end for is_default, segment in segments[::-1] : if segment is None or is_default : segments.pop(-1) @@ -683,47 +936,68 @@ else : break + # should have root left at least assert segments # join url = '/'.join(segment for is_default, segment in segments) # build query args as { key -> [value] } - query_args = [(key, type.build_multi(values[key])) for key, (type, default) in self.query_args.iteritems() if key in values and values[key] is not None] + qargs = [ + ( + key, query_item.build(values) + ) for key, query_item in self.query_items.iteritems() + ] - return "%s%s" % (url, '?%s' % ('&'.join('%s=%s' % (key, value) for key, values in query_args for value in values)) if query_args else '') + # filter + qargs = [(key, values) for key, values in qargs if values is not None] + + return url, qargs def __str__ (self) : + """ + Format pretty representation + """ + return '/'.join(str(label) for label in self.label_path) def __repr__ (self) : + """ + Format debug representation + """ + return "URL(%r, %r)" % (str(self), self.handler) class URLNode (object) : """ - Represents a node in the URLTree + A Node in the URLTree structure, with the Label that it corresponds to. + + Stores the set of URLNodes that exist below this, and optionally the URLRule this node maps to. + + Used by URLTree to resolve an incoming URL. """ - def __init__ (self, parent, label) : + def __init__ (self, parent, label, url_rule=None) : """ Initialize with the given parent and label, empty children dict + + + parent - the URLNode above this node, or None for root. Used to build() URL string + label - the Label matched by this URLNode + url_rule - the URLRule this Node maps to; set later using add_url() with an empty label_path """ - # the parent URLNode + # store self.parent = parent - - # this node's Label self.label = label - + self.url = None + # list of child URLNodes self.children = [] - # this node's URL, set by add_url for an empty label_path - self.url = None - def _build_child (self, label) : """ - Build, insert and return a new child Node + Build, insert and return a new child Node that maps from this Node via the given Label. """ # build new child @@ -735,27 +1009,36 @@ # return return child + def set_url (self, url) : + """ + Set the given URLRule as our mapped target (i.e. corresponds directly to this Node). + + Fails if we already have a mapped URLRule, since we can't have more than one. + """ + + if self.url : + raise URLRuleError(url, "node already has an URLRule mapped") + + # set + self.url = url + def add_url (self, url, label_path) : """ - Add a URL object to this node under the given path. Uses recursion to process the path. + Insert an URLRule into the tree, using recursion to process the given label path, creating new URLNodes as needed. - The label_path argument is a (partial) label path as returned by URL.get_label_path. + url - the URLRule we are storing in the tree + label_path - the path of Labels to the URLRule from this Node down - If label_path is empty (len zero, or begins with EmptyLabel), then the given url is assigned to this node, if no - url was assigned before. + If label_path is empty (len zero, or [EmptyLabel]), then the given url is mapped from this Node using set_url(). """ - # matches this node? - if not label_path or isinstance(label_path[0], EmptyLabel) : - if self.url : - raise URLError(url, "node already defined") - - else : - # set - self.url = url + # resolves to this node? + # XXX: earlier, this was just anywhere, but that would mess up '/foo/bar//quux' def... + if not label_path or (len(label_path) == 1 and isinstance(label_path[0], EmptyLabel)) : + self.set_url(url) else : - # pop child label from label_path + # pop child's label from label_path child_label = label_path.pop(0) # look for the child to recurse into @@ -768,80 +1051,97 @@ break else : - # build a new child + # build and append a new child child = self._build_child(child_label) # recurse to handle the rest of the label_path child.add_url(url, label_path) - def match (self, label_path) : + def match (self, url_segments) : """ - Locate the URL object corresponding to the given label_path value under this node. - - Returns a (url, label_values) tuple + Locate and return the URLRule object corresponding to the given sequence of URL segments value under this node. + + Return a (URLRule, [ LabelValue ] ) tuple, containing the matched URLRule, and all captured values from the URL. """ # determine value to use value = None - # empty label_path? - if not label_path or label_path[0] == '' : + # label_path ends at this node + # XXX: earlier this was just startswith, and would have matched '/foo/quux//bar/asdf' against '/foo/quux' - not sure if desired + # XXX: always accept trailing / ? + if not url_segments or (len(url_segments) == 1 and not url_segments[0]) : # the search ends at this node if self.url : - # this URL is the best match + # this URL is the shortest match return (self.url, []) elif not self.children : # incomplete URL - raise URLError("no URL handler defined for this Node") + raise URLMatchError("No URLRule found for URL") else : - # use default value, i.e. Label.match(None) - label = None + # apply default value, i.e. Label.match(None), and continue recursing + segment = None else : - # pop the next label from the label path - label = label_path.pop(0) + # pop the next URL segment from the label path as the segments to match against our next + segment = url_segments.pop(0) # return one match... match = value = None # recurse through our children, DFS for child in self.children : - # match value - value = child.label.match(label) + # match URL segment against next-down Label rule + match_value = child.label.match(segment) - # skip those that don't match at all - if not value : - continue; + if match_value is True : + # capture without value + pass + + elif not match_value : + # skip those that don't match at all + continue + + # match() gave a capture-tuple + else : + # match_value is (key, value, type, is_default) + value = match_value # already found a match? :/ if match : - raise URLError("Ambiguous URL") - - # ok, but continue looking to make sure there's no ambiguous URLs - match = child - + # matches against multiple URLRules + # XXX: just return first defined rule instead? + # XXX: include troublesome nodes in error to aid debugging + raise URLMatchError("Ambiguous URL: %s <-> %s" % (match, child)) + + else : + # ok, but continue looking to make sure there's no ambiguous URLs + match = child + # found something? if not match : - raise URLError("No child found for label: %s + %s + %s" % (self.get_url(), label, '/'.join(str(l) for l in label_path))) + # 404, kind of + raise URLMatchError("URL not found: %s -> %s + %s" % (self.get_url(), segment, '/'.join(str(seg) for seg in url_segments))) + + # recurse into the match using the remaining segments to get the rest of the values + url, url_values = match.match(url_segments) - # ok, recurse into the match - url, label_value = match.match(label_path) - - # add our value? - if isinstance(value, LabelValue) : - label_value.append(value) + # captured a value? + if value is not None : + # add it to the set of values captured below us + url_values.append(value) # return the match - return url, label_value + return url, url_values def get_url (self) : """ - Returns the URL for this node, by iterating over our parents + Returns the URLRule spec leading to this node, by iterating over our parents. """ - # URL segments in reverse order + # URL segments in reverse order, ending with a / segments = [''] # start with ourself @@ -849,7 +1149,10 @@ # iterate up to root while node : - segments.append(str(node.label)) + # URLRule spec + spec = str(node.label) + + segments.append(spec) node = node.parent @@ -861,7 +1164,7 @@ def dump (self, indent=0) : """ - Returns a multi-line string representation of this Node + Returns a multi-line nested-node string representation of this Node """ return '\n'.join([ @@ -874,68 +1177,80 @@ ]) def __str__ (self) : - return "%s/[%s]" % (self.label, ','.join(str(child) for child in self.children)) - -class URLTree (handler.RequestHandler) : - """ - Map requests to handlers, using a defined tree of URLs - """ - - def __init__ (self, url_list) : """ - Initialize the tree using the given list of URLs + Return a short representation of this Node's match sequence, and our children. """ - # root node + # XXX: recurse for unit-test purposes... + return "%s/[%s]" % (self.label, ','.join(str(child) for child in self.children)) + +class URLTree (object) : + """ + Map incoming URLs to URLRules and handlers, using a defined tree of URLNodes. + """ + + def __init__ (self, url_rules) : + """ + Initialize the tree using the given sequence of URLRules + """ + + # root node, matching '/' self.root = URLNode(None, EmptyLabel()) - # just add each URL - for url in url_list : + # insert each URL into the tree + for url in url_rules : self.add_url(url) - def add_url (self, url) : + def add_url (self, url_rule) : """ - Adds the given URL to the tree. The URL must begin with a root slash. + Adds the given URLRule to the tree. The URLRule must begin with a root slash. """ - # get url's label path - path = url.get_label_path() + # get url's Label path + path = url_rule.get_label_path() - # add to root - self.root.add_url(url, path) + # insert into tree by path + self.root.add_url(url_rule, path) def match (self, url) : """ - Find the URL object best corresponding to the given url, matching any ValueLabels. - - Returns an (URL, [LabelValue]) tuple. - - XXX: handle unicode on URLs - """ + Find the URL object corresponding to the given incoming url, matching any ValueLabels. - # split it into labels - path = url.split('/') - - # empty URL is empty - if url : - # ensure that it doesn't start with a / - assert not self.root.label.match(path[0]), "URL must not begin with root" + Returns an (URLRule, [(value, label, is_default)]) tuple. - # just match starting at root - return self.root.match(path) - - def handle_request (self, request) : - """ - Looks up the request's URL, and invokes its handler + XXX: handle unicode on URLs? """ - # get the requested URL - request_url = request.get_page_name() + # remove leading root, since that's where we always start + url = url.lstrip('/') - # find the URL+values to use - url, label_values = self.match(request_url) + # split it into segments + segments = url.split('/') + + # recurse into the tree + return self.root.match(segments) - # let the URL handle it - return url.execute(request, label_values) + def evaluate (self, url, qargs=None) : + """ + Look up the URLRule's handler for the given incoming url: + url - the text-form URL + qargs - a [ (key, [ value ] ) ] list of query string arguments + + and return: + * the `handler` configured for the URLRule + * a { key: value } dict of URL values suitable for use as **kwargs + * a [ (key, [ value ] ) ] list of remaining query string arguments not included in the returned keyword + args + + """ + + # lookup + rule, url_values = self.match(url) + + # evaluate + args, qargs = rule.evaluate(url_values, qargs) + + return rule.handler, args, qargs + diff -r 30af52a271a1 -r e3001377e9dc tests/ext_urltree.py --- a/tests/ext_urltree.py Sun Jan 09 00:50:21 2011 +0200 +++ b/tests/ext_urltree.py Sun Jan 09 15:52:28 2011 +0200 @@ -7,15 +7,16 @@ from svv.ext import urltree -class TestLabelValue (unittest.TestCase) : - class dummylabel : - key = 'foo' +if False : + class TestLabelValue (unittest.TestCase) : + class dummylabel : + key = 'foo' - def test_str_default (self) : - self.assertEqual(str(urltree.LabelValue(self.dummylabel, 'bar', True)), "foo") + def test_str_default (self) : + self.assertEqual(str(urltree.LabelValue(self.dummylabel, 'bar', True)), "foo") - def test_str_value (self) : - self.assertEqual(str(urltree.LabelValue(self.dummylabel, 'bar', False)), "foo='bar'") + def test_str_value (self) : + self.assertEqual(str(urltree.LabelValue(self.dummylabel, 'bar', False)), "foo='bar'") class TestLabel (unittest.TestCase) : def setUp (self) : @@ -44,22 +45,22 @@ self.assertRaises(urltree.URLError, urltree.Label.parse, "foo/bar", {}, self.config) def test_parse_value_plain (self) : - self._test_parse("{foo}", urltree.SimpleValueLabel, key="foo", default=None, type_name=None, type=urltree.URLConfig.BUILTIN_TYPES[None]) + self._test_parse("{foo}", urltree.ValueLabel, key="foo", default=None, type_name=None, type=urltree.URLConfig.BUILTIN_TYPES[None]) def test_parse_value_default1 (self) : - self._test_parse("{foo=bar1}", urltree.SimpleValueLabel, key="foo", default="bar1", type_name=None) + self._test_parse("{foo=bar1}", urltree.ValueLabel, key="foo", default="bar1", type_name=None) def test_parse_value_default2 (self) : - self._test_parse("{foo}", urltree.SimpleValueLabel, dict(foo="bar2"), key="foo", default="bar2", type_name=None) + self._test_parse("{foo}", urltree.ValueLabel, dict(foo="bar2"), key="foo", default="bar2", type_name=None) def test_parse_value_default3 (self) : - self._test_parse("{foo=bar1}", urltree.SimpleValueLabel, dict(foo="bar3"), key="foo", default="bar3", type_name=None) + self._test_parse("{foo=bar1}", urltree.ValueLabel, dict(foo="bar3"), key="foo", default="bar3", type_name=None) def test_parse_value_type_str (self) : - self._test_parse("{foo:str}", urltree.SimpleValueLabel, key="foo", type_name="str", type=urltree.URLConfig.BUILTIN_TYPES['str']) + self._test_parse("{foo:str}", urltree.ValueLabel, key="foo", type_name="str", type=urltree.URLConfig.BUILTIN_TYPES['str']) def test_parse_value_type_int (self) : - self._test_parse("{foo:int}", urltree.SimpleValueLabel, key="foo", type_name="int", type=urltree.URLConfig.BUILTIN_TYPES['int']) + self._test_parse("{foo:int}", urltree.ValueLabel, key="foo", type_name="int", type=urltree.URLConfig.BUILTIN_TYPES['int']) def test_parse_invalid_type (self) : self.assertRaises(KeyError, self._test_parse, "{foo:bar}", None) @@ -109,27 +110,29 @@ def test_str (self) : self.assertEqual(str(self.label), "test") -class TestSimpleValueLabel (unittest.TestCase) : +class TestValueLabel (unittest.TestCase) : def setUp (self) : - self.label = urltree.SimpleValueLabel("test", 'int', urltree.URLConfig.BUILTIN_TYPES['int'], None) - self.label_default_0 = urltree.SimpleValueLabel("test", 'int', urltree.URLConfig.BUILTIN_TYPES['int'], 0) - self.label_default_1 = urltree.SimpleValueLabel("test", 'int', urltree.URLConfig.BUILTIN_TYPES['int'], 1) - self.label_str = urltree.SimpleValueLabel("test", None, urltree.URLConfig.BUILTIN_TYPES[None], None) - self.label_str_default = urltree.SimpleValueLabel("test", None, urltree.URLConfig.BUILTIN_TYPES[None], 'def') + self.label = urltree.ValueLabel("test", urltree.URLConfig.BUILTIN_TYPES['int'], None, 'int') + self.label_default_0 = urltree.ValueLabel("test", urltree.URLConfig.BUILTIN_TYPES['int'], 0, 'int') + self.label_default_1 = urltree.ValueLabel("test", urltree.URLConfig.BUILTIN_TYPES['int'], 1, 'int') + self.label_str = urltree.ValueLabel("test", urltree.URLConfig.BUILTIN_TYPES[None], None, None) + self.label_str_default = urltree.ValueLabel("test", urltree.URLConfig.BUILTIN_TYPES[None], 'def', None) def test_eq (self) : - self.assertTrue(self.label == urltree.SimpleValueLabel("test", 'str', None, 1)) + self.assertTrue(self.label == urltree.ValueLabel("test", None, 1, 'str')) self.assertFalse(self.label == urltree.StaticLabel("Test")) self.assertFalse(self.label == urltree.EmptyLabel()) def _check_value (self, label, label_value, value, is_default) : - self.assertTrue(isinstance(label_value, urltree.LabelValue)) - self.assertEqual(label_value.label, label) - self.assertEqual(label_value.value, value) - self.assertEqual(label_value.is_default, is_default) + _key, _value, _type, _is_default = label_value + + self.assertEqual(_key, label.key) + self.assertEqual(_type,label.type) + self.assertEqual(_value, value) + self.assertEqual(_is_default, is_default) def test_match_default_none (self) : - self.assertEquals(self.label.match(), None) + self.assertEquals(self.label.match(), False) def test_match_default_value (self) : self._check_value(self.label_default_0, self.label_default_0.match(), 0, True) @@ -190,7 +193,9 @@ def test_build (self) : self.assertEqual(self.type.build(""), "") self.assertEqual(self.type.build("xxx"), "xxx") - self.assertEqual(self.type.build("äää"), "äää") + + # XXX: unicode? + self.assertEqual(self.type.build(u"äää"), u"äää") class TestIntegerType (unittest.TestCase) : def setUp (self) : @@ -210,8 +215,8 @@ self.assertFalse(self.type_max_5.test("6")) def test_parse_invalid (self) : - self.assertRaises(ValueError, self.type.parse, "xx") - self.assertRaises(ValueError, self.type_nonzero.parse, "0") + self.assertRaises(urltree.URLValueError, self.type.parse, "xx") + self.assertRaises(urltree.URLValueError, self.type_nonzero.parse, "0") def test_parse_valid (self) : self.assertEqual(self.type.parse("0"), 0) @@ -219,7 +224,7 @@ self.assertEqual(self.type_nonzero.parse("3"), 3) def test_append (self) : - self.assertRaises(urltree.URLError, self.type.append, 0, 1) + self.assertRaises(urltree.URLValueError, self.type.append, 0, 1) def test_build (self) : self.assertEqual(self.type.build(0), "0") @@ -229,9 +234,9 @@ self.assertEqual(self.type_max_5.build(5), "5") def test_build_invalid (self) : - self.assertRaises(ValueError, self.type_positive.build, -1) - self.assertRaises(ValueError, self.type_nonzero.build, 0) - self.assertRaises(ValueError, self.type_max_5.build, 6) + self.assertRaises(urltree.URLValueError, self.type_positive.build, -1) + self.assertRaises(urltree.URLValueError, self.type_nonzero.build, 0) + self.assertRaises(urltree.URLValueError, self.type_max_5.build, 6) def test_build_multi (self) : self.assertEqual(self.type.build_multi(0), ["0"]) @@ -252,19 +257,19 @@ class TestConfig (unittest.TestCase) : def test_init (self) : - urltree.URLConfig(type_dict=dict(foo=None), ignore_extra_args=True) + urltree.URLConfig(type_dict=dict(foo=None), reject_extra_args=False) def test_get_type (self) : self.assertEquals(urltree.URLConfig(dict(foo='xxx')).get_type('foo'), 'xxx') def test_get_type_invalid (self) : - self.assertRaises(KeyError, urltree.URLConfig(dict(foo='xxx')).get_type, 'xxx') + self.assertRaises(KeyError, urltree.URLConfig(dict(foo='xxx')).get_type, 'bar') def test_call (self) : config = urltree.URLConfig() url = config("foo", None) - self.assertTrue(isinstance(url, urltree.URL)) + self.assertTrue(isinstance(url, urltree.URLRule)) self.assertTrue(url in config.urls) def test_iter (self) : @@ -274,13 +279,13 @@ urls = list(config) - self.assertTrue(urls[0].url_mask == "foo1") - self.assertTrue(urls[1].url_mask == "foo2") + self.assertTrue(urls[0].url_spec == "foo1") + self.assertTrue(urls[1].url_spec == "foo2") class TestURL (unittest.TestCase) : def setUp (self) : - self.config = urltree.URLConfig(ignore_extra_args=True) - self.config_strict = urltree.URLConfig(ignore_extra_args=False) + self.config = urltree.URLConfig(reject_extra_args=False) +# self.config_strict = urltree.URLConfig(ignore_extra_args=False) def _test_label_path (self, mask, *path, **qargs) : url = self.config(mask, None) @@ -289,11 +294,11 @@ self.assertEquals(url.label_path, list(path)) # right qargs keys - self.assertEquals(set(url.query_args), set(qargs)) + self.assertEquals(set(url.query_items), set(qargs)) # right qargs values for key, value in qargs.iteritems() : - self.assertEquals(url.query_args[key], value) + self.assertEquals((url.query_items[key].type, url.query_items[key].default), value) # __init__ def test_label_path_empty (self) : @@ -309,7 +314,7 @@ self._test_label_path("/foo/bar/", urltree.StaticLabel("foo"), urltree.StaticLabel("bar"), urltree.EmptyLabel()) def test_label_path_mix (self) : - self._test_label_path("/foo/{bar}", urltree.StaticLabel("foo"), urltree.SimpleValueLabel("bar", None, None, None)) + self._test_label_path("/foo/{bar}", urltree.StaticLabel("foo"), urltree.ValueLabel("bar", None, None, None)) # def test_query_args_root_empty (self) : # self._test_label_path("/?", urltree.EmptyLabel()) @@ -347,32 +352,26 @@ return url - class dummyrequest : - def __init__ (self, qargs) : self.qargs = qargs - def get_args (self) : return self.qargs - + # XXX: not used? class dummy_label : def __init__ (self, key, type) : self.key = key self.type = type - class dummy_labelvalue : - def __init__ (self, key, value, is_default, type) : - self.label = TestURL.dummy_label(key, type) - self.value = value - self.is_default = is_default - + # XXX: rename to test_evaluate def _test_execute (self, mask, values={}, qargs={}, qlist=[], config=None) : if not config : config = self.config # setup url = self._setup_execute(mask, config) - req = self.dummyrequest(qargs.items() + qlist) - values = [self.dummy_labelvalue(k, v, d, config.get_type()) for k, (v, d) in values.iteritems()] + + # qargs + uargs = [(key, value, config.get_type(), is_default) for key, (value, is_default) in values.iteritems()] + qargs = [(key, [value]) for key, value in qargs.iteritems()] + qlist # exec - out_args = url.execute(req, values) + out_args, remaining = url.evaluate(uargs, qargs) return out_args @@ -402,10 +401,10 @@ self.assertEquals(self._test_execute("/{foo}/?bar=", dict(foo=("x", False))), dict(foo="x")) def test_execute_qargs_missing (self) : - self.assertRaises(urltree.URLError, self._test_execute, "/{foo}/?bar", dict(foo=("x", False))) + self.assertRaises(urltree.URLValueError, self._test_execute, "/{foo}/?bar", dict(foo=("x", False))) def test_execute_qargs_invalid (self) : - self.assertRaises(ValueError, self._test_execute, "/{foo}/?bar:int", dict(foo=("x", False)), dict(bar="x")) + self.assertRaises(urltree.URLValueError, self._test_execute, "/{foo}/?bar:int", dict(foo=("x", False)), dict(bar="x")) def test_execute_qargs_simple (self) : self.assertEquals(self._test_execute("/{foo}/?bar", dict(foo=("x", False)), dict(bar="y")), dict(foo="x", bar="y")) @@ -414,30 +413,35 @@ self.assertRaises(urltree.URLError, self._test_execute, "/{foo}/?bar", dict(foo=("x", False)), dict(bar='')) def test_execute_qargs_multi_invalid (self) : - self.assertRaises(urltree.URLError, self._test_execute, "/{foo}/?bar", dict(foo=("x", False)), qlist=[('bar', 'a'), ('bar', 'b')]) + self.assertRaises(urltree.URLError, self._test_execute, "/{foo}/?bar", dict(foo=("x", False)), qlist=[('bar', ['a', 'b'])]) def test_execute_qargs_multi_list (self) : - self.assertEqual(self._test_execute("/{foo}/?bar:list", dict(foo=("x", False)), qlist=[('bar', 'a'), ('bar', 'b')]), dict(foo='x', bar=['a', 'b'])) - - def test_execute_qarg_override_strict (self) : - self.assertRaises(urltree.URLError, self._test_execute, "/{foo}", dict(foo=("x1", False)), dict(foo="x2"), config=self.config_strict) + self.assertEqual(self._test_execute("/{foo}/?bar:list", dict(foo=("x", False)), qlist=[('bar', ['a', 'b'])]), dict(foo='x', bar=['a', 'b'])) + + # XXX: fix to check remaining qargs +# def test_execute_qarg_override_strict (self) : +# self.assertRaises(urltree.URLError, self._test_execute, "/{foo}", dict(foo=("x1", False)), dict(foo="x2"), config=self.config_strict) def test_execute_qarg_override_ignore (self) : + """Query-arg does not override non-default Label value""" self.assertEqual(self._test_execute("/{foo}", dict(foo=("x1", False)), dict(foo="x2")), dict(foo="x1")) def test_execute_qarg_override_ok (self) : + """Query-arg overrides default Label value""" self.assertEqual(self._test_execute("/{foo=x1}", dict(foo=("x1", True)), dict(foo="x2")), dict(foo="x2")) # build - class dummyrequest_page : - def __init__ (self, page_prefix) : - self.page_prefix = page_prefix - + def _test_build (self, mask, url, **args) : - self.assertEquals(self.config(mask, None).build(self.dummyrequest_page("/index.cgi"), **args), "/index.cgi" + url) + out, qargs = self.config(mask, None).build(**args) + + if qargs : + out += ('?' + '&'.join('%s=%s' % (key, value) for key, values in qargs for value in values)) + + self.assertEquals(out, url) def _test_build_fails (self, err, mask, **args) : - self.assertRaises(err, self.config(mask, None).build, self.dummyrequest_page("/index.cgi"), **args) + self.assertRaises(err, self.config(mask, None).build, **args) def test_build_empty (self) : self._test_build("/", "/") @@ -487,7 +491,7 @@ class TestTreeBuild (unittest.TestCase) : def setUp (self) : - self.config = urltree.URLConfig(ignore_extra_args=True) + self.config = urltree.URLConfig(reject_extra_args=False) def test_simple_root (self) : self.config("/", None) @@ -525,7 +529,7 @@ class TestTreeMatch (unittest.TestCase) : def setUp (self) : - self.config = urltree.URLConfig(ignore_extra_args=True) + self.config = urltree.URLConfig(reject_extra_args=False) self.root =self.config("/", None) self.bar = self.config("/bar", None) @@ -540,10 +544,10 @@ self.assertEqual(t_url, url) - self.assertEqual(set(v.label.key for v in t_values), set(values)) + self.assertEqual(set(key for key, value, type, is_default in t_values), set(values)) - for v in t_values : - self.assertEqual(v.value, values[v.label.key]) + for key, value, type, is_default in t_values : + self.assertEqual(value, values[key]) def test_root (self) : self._test_match("", self.root) @@ -571,13 +575,10 @@ class TestTreeHandler (unittest.TestCase) : def _build_handler (self, name) : - def _handler (req, **args) : - return name, args - - return _handler + return name def setUp (self) : - self.config = urltree.URLConfig(ignore_extra_args=True) + self.config = urltree.URLConfig(reject_extra_args=False) self.root =self.config("/", self._build_handler('root')) self.bar = self.config("/bar", self._build_handler('bar')) @@ -586,18 +587,10 @@ self.tree = urltree.URLTree(self.config) - class dummyrequest_page : - def __init__ (self, page_name, qargs) : - self.page_name = page_name - self.qargs = qargs + def _test_handle (self, path, name, qargs={}, **args) : + qargs = [(key, [value]) for key, value in qargs.iteritems()] - def get_page_name (self) : return self.page_name - def get_args (self) : return self.qargs - - def _test_handle (self, path, name, qargs={}, **args) : - req = self.dummyrequest_page(path, qargs.iteritems()) - - h_name, h_args = self.tree.handle_request(req) + h_name, h_args, q_args = self.tree.evaluate(path, qargs) self.assertEqual(h_name, name) self.assertEqual(h_args, args)