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