ext.urltree: improved docs and refactoring
authorTero Marttila <terom@fixme.fi>
Sun, 09 Jan 2011 15:52:28 +0200
changeset 45 e3001377e9dc
parent 44 30af52a271a1
child 46 547940cb0e1c
ext.urltree: improved docs and refactoring
svv/ext/urltree.py
tests/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<name>[a-zA-Z0-9_.-]+)$')
+    PARSE_RE = re.compile(r'^(?P<name>[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
     """
+    
+    # / "{" <key> [ ":" <type> ] [ "=" [ <default> ] ] "}" /
+    PARSE_RE = 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, 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<key>[a-zA-Z_][a-zA-Z0-9_]*)(:(?P<type>[a-zA-Z_][a-zA-Z0-9_]*))?(=(?P<default>[^}]*))?\}$')
-
-    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
 
 
+
--- 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)