txaws-dev team mailing list archive
-
txaws-dev team
-
Mailing list archive
-
Message #00111
[Merge] lp:~radix/txaws/parameter-enrichment into lp:txaws
Christopher Armstrong has proposed merging lp:~radix/txaws/parameter-enrichment into lp:txaws.
Requested reviews:
txAWS Technical List (txaws-tech)
txAWS Technical List (txaws-tech)
Related bugs:
Bug #984660 in txAWS: "Enrich schema declarations to allow for describing all the details of an API"
https://bugs.launchpad.net/txaws/+bug/984660
For more details, see:
https://code.launchpad.net/~radix/txaws/parameter-enrichment/+merge/103592
This is one part of schema enrichment. It adds List and Structure Parameter types, and significantly refactors the way that paramater parsing and formatting is done.
--
https://code.launchpad.net/~radix/txaws/parameter-enrichment/+merge/103592
Your team txAWS Technical List is requested to review the proposed merge of lp:~radix/txaws/parameter-enrichment into lp:txaws.
=== modified file 'txaws/server/schema.py'
--- txaws/server/schema.py 2012-03-27 11:51:09 +0000
+++ txaws/server/schema.py 2012-04-26 01:41:19 +0000
@@ -26,6 +26,12 @@
super(MissingParameterError, self).__init__(message)
+class InconsistentParameterError(SchemaError):
+ def __init__(self, name):
+ message = "Parameter %s is used inconsistently" % (name,)
+ super(InconsistentParameterError, self).__init__(message)
+
+
class InvalidParameterValueError(SchemaError):
"""Raised when the value of a parameter is invalid."""
@@ -51,6 +57,20 @@
super(UnknownParameterError, self).__init__(message)
+class UnknownParametersError(Exception):
+ """
+ Raised when extra unknown fields are passed to L{Structure.parse}.
+
+ @ivar result: The already coerced result representing the known parameters.
+ @ivar unknown: The unknown parameters.
+ """
+ def __init__(self, result, unknown):
+ self.result = result
+ self.unknown = unknown
+ message = "The parameters %s are not recognized" % (unknown,)
+ super(UnknownParametersError, self).__init__(message)
+
+
class Parameter(object):
"""A single parameter in an HTTP request.
@@ -67,7 +87,9 @@
@param validator: A callable to validate the parameter, returning a bool.
"""
- def __init__(self, name, optional=False, default=None,
+ supports_multiple = False
+
+ def __init__(self, name=None, optional=False, default=None,
min=None, max=None, allow_none=False, validator=None):
self.name = name
self.optional = optional
@@ -182,7 +204,7 @@
lower_than_min_template = "Value must be at least %s."
greater_than_max_template = "Value exceeds maximum of %s."
- def __init__(self, name, optional=False, default=None,
+ def __init__(self, name=None, optional=False, default=None,
min=0, max=None, allow_none=False, validator=None):
super(Integer, self).__init__(name, optional, default, min, max,
allow_none, validator)
@@ -228,8 +250,10 @@
kind = "enum"
- def __init__(self, name, mapping, optional=False, default=None):
+ def __init__(self, name=None, mapping=None, optional=False, default=None):
super(Enum, self).__init__(name, optional=optional, default=default)
+ if mapping is None:
+ raise TypeError("Must provide mapping")
self.mapping = mapping
self.reverse = dict((value, key) for key, value in mapping.iteritems())
@@ -260,18 +284,145 @@
return datetime.strftime(utc_value, "%Y-%m-%dT%H:%M:%SZ")
+class List(Parameter):
+ """
+ A homogenous list of instances of a parameterized type.
+
+ There is a strange behavior that lists can have any starting index and any
+ gaps are ignored. Conventionally they are 1-based, and so indexes proceed
+ like 1, 2, 3... However, any non-negative index can be used and the
+ ordering will be used to determine the true index. So::
+
+ {5: 'a', 7: 'b', 9: 'c'}
+
+ becomes::
+
+ ['a', 'b', 'c']
+ """
+
+ kind = "list"
+ supports_multiple = True
+
+ def __init__(self, name=None, item=None, optional=False, default=None):
+ """
+ @param item: A L{Parameter} instance which will be used to parse and
+ format the values in the list.
+ """
+ if item is None:
+ raise TypeError("Must provide item")
+ super(List, self).__init__(name, optional=optional, default=default)
+ self.item = item
+
+ def parse(self, value):
+ """
+ Convert a dictionary of {relative index: value} to a list of parsed
+ C{value}s.
+ """
+ indices = []
+ if not isinstance(value, dict):
+ raise InvalidParameterValueError("%r should be a dict." % (value,))
+ for index in value.keys():
+ try:
+ indices.append(int(index))
+ except ValueError:
+ raise UnknownParameterError(index)
+ result = [None] * len(value)
+ for index_index, index in enumerate(sorted(indices)):
+ v = value[str(index)]
+ if index < 0:
+ raise UnknownParameterError(index)
+ result[index_index] = self.item.coerce(v)
+ return result
+
+ def format(self, value):
+ """
+ Convert a list like::
+
+ ["a", "b", "c"]
+
+ to:
+
+ {"1": "a", "2": "b", "3": "c"}
+
+ C{value} may also be an L{Arguments} instance, mapping indices to
+ values. Who knows why.
+ """
+ if isinstance(value, Arguments):
+ return dict((str(i), self.item.format(v)) for i, v in value)
+ return dict((str(i + 1), self.item.format(v))
+ for i, v in enumerate(value))
+
+
+class Structure(Parameter):
+ """
+ A structure with named fields of parameterized types.
+ """
+
+ kind = "structure"
+ supports_multiple = True
+
+ def __init__(self, name=None, fields=None, optional=False, default=None):
+ """
+ @param fields: A mapping of field name to field L{Parameter} instance.
+ """
+ if fields is None:
+ raise TypeError("Must provide fields")
+ super(Structure, self).__init__(name, optional=optional,
+ default=default)
+ self.fields = fields
+
+ def parse(self, value):
+ """
+ Convert a dictionary of raw values to a dictionary of processed values.
+ """
+ result = {}
+ rest = {}
+ for k, v in value.iteritems():
+ if k in self.fields:
+ if (isinstance(v, dict)
+ and not self.fields[k].supports_multiple):
+ if len(v) == 1:
+ # We support "foo.1" as "foo" as long as there is only
+ # one "foo.#" parameter provided.... -_-
+ v = v.values()[0]
+ else:
+ raise InvalidParameterCombinationError(k)
+ result[k] = self.fields[k].coerce(v)
+ else:
+ rest[k] = v
+ for k, v in self.fields.iteritems():
+ if k not in result:
+ result[k] = v.coerce(None)
+ if rest:
+ raise UnknownParametersError(result, rest)
+ return result
+
+ def format(self, value):
+ """
+ Convert a dictionary of processed values to a dictionary of raw values.
+ """
+ if not isinstance(value, Arguments):
+ value = value.iteritems()
+ return dict((k, self.fields[k].format(v)) for k, v in value)
+
+
class Arguments(object):
"""Arguments parsed from a request."""
def __init__(self, tree):
"""Initialize a new L{Arguments} instance.
- @param tree: The C{dict}-based structure of the L{Argument}instance
+ @param tree: The C{dict}-based structure of the L{Argument} instance
to create.
"""
for key, value in tree.iteritems():
self.__dict__[key] = self._wrap(value)
+ def __str__(self):
+ return "Arguments(%s)" % (self.__dict__,)
+
+ __repr__ = __str__
+
def __iter__(self):
"""Returns an iterator yielding C{(name, value)} tuples."""
return self.__dict__.iteritems()
@@ -288,7 +439,7 @@
"""Wrap the given L{tree} with L{Arguments} as necessary.
@param tree: A {dict}, containing L{dict}s and/or leaf values, nested
- arbitrarily deep.
+ arbitrarily deep.
"""
if isinstance(value, dict):
if any(isinstance(name, int) for name in value.keys()):
@@ -299,6 +450,8 @@
return [self._wrap(value) for (name, value) in items]
else:
return Arguments(value)
+ elif isinstance(value, list):
+ return [self._wrap(x) for x in value]
else:
return value
@@ -308,7 +461,7 @@
The schema that the arguments of an HTTP request must be compliant with.
"""
- def __init__(self, *parameters):
+ def __init__(self, *_parameters, **kwargs):
"""Initialize a new L{Schema} instance.
Any number of L{Parameter} instances can be passed. The parameter path
@@ -323,60 +476,34 @@
A more complex example::
- schema = Schema(Unicode('Name.#'))
+ schema = Schema(List('Names', item=Unicode()))
- means that the result of L{Schema.extract} would have a C{Name}
+ means that the result of L{Schema.extract} would have a C{Names}
attribute, which would itself contain a list of names. Similarly,
- L{Schema.bundle} would look for a C{Name} attribute.
+ L{Schema.bundle} would look for a C{Names} attribute.
"""
- self._parameters = dict(
- (self._get_template(parameter.name), parameter)
- for parameter in parameters)
+ if 'parameters' in kwargs:
+ if len(_parameters) > 0:
+ raise TypeError("parameters= must only be passed "
+ "without positional arguments")
+ self._parameters = kwargs['parameters']
+ else:
+ self._parameters = self._convert_old_schema(_parameters)
def extract(self, params):
"""Extract parameters from a raw C{dict} according to this schema.
@param params: The raw parameters to parse.
- @return: An L{Arguments} object holding the extracted arguments.
-
- @raises UnknownParameterError: If C{params} contains keys that this
- schema doesn't know about.
+ @return: A tuple of an L{Arguments} object holding the extracted
+ arguments and any unparsed arguments.
"""
- tree = {}
- rest = {}
-
- # Extract from the given arguments and parse according to the
- # corresponding parameters.
- for name, value in params.iteritems():
- template = self._get_template(name)
- parameter = self._parameters.get(template)
-
- if template.endswith(".#") and parameter is None:
- # If we were unable to find a direct match for a template that
- # allows multiple values. Let's attempt to find it without the
- # multiple value marker which Amazon allows. For example if the
- # template is 'PublicIp', then a single key 'PublicIp.1' is
- # allowed.
- parameter = self._parameters.get(template[:-2])
- if parameter is not None:
- name = name[:-2]
-
- # At this point, we have a template that doesn't have the .#
- # marker to indicate multiple values. We don't allow multiple
- # "single" values for the same element.
- if name in tree.keys():
- raise InvalidParameterCombinationError(name)
-
- if parameter is None:
- rest[name] = value
- else:
- self._set_value(tree, name, parameter.coerce(value))
-
- # Ensure that the tree arguments are consistent with constraints
- # defined in the schema.
- for template, parameter in self._parameters.iteritems():
- self._ensure_tree(tree, parameter, *template.split("."))
-
+ structure = Structure(fields=self._parameters)
+ try:
+ tree = structure.coerce(self._convert_flat_to_nest(params))
+ rest = {}
+ except UnknownParametersError, error:
+ tree = error.result
+ rest = error.unknown
return Arguments(tree), rest
def bundle(self, *arguments, **extra):
@@ -390,117 +517,86 @@
params = {}
for argument in arguments:
- self._flatten(params, argument)
- self._flatten(params, extra)
+ params.update(argument)
+ params.update(extra)
+ result = {}
for name, value in params.iteritems():
- parameter = self._parameters.get(self._get_template(name))
+ if value is None:
+ continue
+ segments = name.split('.')
+ first = segments[0]
+ parameter = self._parameters.get(first)
if parameter is None:
raise RuntimeError("Parameter '%s' not in schema" % name)
else:
if value is None:
- params[name] = ""
- else:
- params[name] = parameter.format(value)
-
- return params
-
- def _get_template(self, key):
- """Return the canonical template for a given parameter key.
-
- For example::
-
- 'Child.1.Name.2'
-
- becomes::
-
- 'Child.#.Name.#'
-
- """
- parts = key.split(".")
- for index, part in enumerate(parts[1::2]):
- parts[index * 2 + 1] = "#"
- return ".".join(parts)
-
- def _set_value(self, tree, path, value):
- """Set C{value} at C{path} in the given C{tree}.
-
- For example::
-
- tree = {}
- _set_value(tree, 'foo.1.bar.2', True)
-
- results in C{tree} becoming::
-
- {'foo': {1: {'bar': {2: True}}}}
-
- @param tree: A L{dict}.
- @param path: A L{str}.
- @param value: The value to set. Can be anything.
- """
- nodes = []
- for index, node in enumerate(path.split(".")):
- if index % 2:
- # Nodes with odd indexes must be non-negative integers
- try:
- node = int(node)
- except ValueError:
- raise UnknownParameterError(path)
- if node < 0:
- raise UnknownParameterError(path)
- nodes.append(node)
- for node in nodes[:-1]:
- tree = tree.setdefault(node, {})
- tree[nodes[-1]] = value
-
- def _ensure_tree(self, tree, parameter, node, *nodes):
- """Check that C{node} exists in C{tree} and is followed by C{nodes}.
-
- C{node} and C{nodes} should correspond to a template path (i.e. where
- there are no absolute indexes, but C{#} instead).
- """
- if node == "#":
- if len(nodes) == 0:
- if len(tree.keys()) == 0 and not parameter.optional:
- raise MissingParameterError(parameter.name)
- else:
- for subtree in tree.itervalues():
- self._ensure_tree(subtree, parameter, *nodes)
- else:
- if len(nodes) == 0:
- if node not in tree.keys():
- # No value for this parameter is present, if it's not
- # optional nor allow_none is set, the call below will
- # raise a MissingParameterError
- tree[node] = parameter.coerce(None)
- else:
- if node not in tree.keys():
- tree[node] = {}
- self._ensure_tree(tree[node], parameter, *nodes)
-
- def _flatten(self, params, tree, path=""):
- """
- For every element in L{tree}, set C{path} to C{value} in the given
- L{params} dictionary.
-
- @param params: A L{dict} which will be populated.
- @param tree: A structure made up of L{Argument}s, L{list}s, L{dict}s
- and leaf values.
- """
- if isinstance(tree, Arguments):
- for name, value in tree:
- self._flatten(params, value, "%s.%s" % (path, name))
- elif isinstance(tree, dict):
- for name, value in tree.iteritems():
- self._flatten(params, value, "%s.%s" % (path, name))
- elif isinstance(tree, list):
- for index, value in enumerate(tree):
- self._flatten(params, value, "%s.%d" % (path, index + 1))
- elif tree is not None:
- params[path.lstrip(".")] = tree
- else:
- # None is discarded.
- pass
+ result[name] = ""
+ else:
+ result[name] = parameter.format(value)
+
+ return self._convert_nest_to_flat(result)
+
+ def _convert_flat_to_nest(self, params):
+ """
+ Convert a structure in the form of::
+
+ {'foo.1.bar': 'value',
+ 'foo.2.baz': 'value'}
+
+ to::
+
+ {'foo': {'1': {'bar': 'value'},
+ '2': {'baz': 'value'}}}
+
+ This is intended for use both during parsing of HTTP arguments like
+ 'foo.1.bar=value' and when dealing with schema declarations that look
+ like 'foo.n.bar'.
+
+ This is the inverse of L{_convert_nest_to_flat}.
+ """
+ result = {}
+ for k, v in params.iteritems():
+ last = result
+ segments = k.split('.')
+ for index, item in enumerate(segments):
+ if index == len(segments) - 1:
+ newd = v
+ else:
+ newd = {}
+ if not isinstance(last, dict):
+ raise InconsistentParameterError(k)
+ if type(last.get(item)) is dict and type(newd) is not dict:
+ raise InconsistentParameterError(k)
+ last = last.setdefault(item, newd)
+ return result
+
+ def _convert_nest_to_flat(self, params, _result=None, _prefix=None):
+ """
+ Convert a data structure that looks like::
+
+ {"foo": {"bar": "baz", "shimmy": "sham"}}
+
+ to::
+
+ {"foo.bar": "baz",
+ "foo.shimmy": "sham"}
+
+ This is the inverse of L{_convert_flat_to_nest}.
+ """
+ if _result is None:
+ _result = {}
+ for k, v in params.iteritems():
+ if _prefix is None:
+ path = k
+ else:
+ path = _prefix + '.' + k
+ if isinstance(v, dict):
+ return self._convert_nest_to_flat(v, _result=_result,
+ _prefix=path)
+ else:
+ _result[path] = v
+ return _result
def extend(self, *schema_items):
"""
@@ -513,3 +609,49 @@
else:
raise TypeError("Illegal argument %s" % item)
return Schema(*parameters)
+
+ def _convert_old_schema(self, parameters):
+ """
+ Convert an ugly old schema, using dotted names, to the hot new schema,
+ using List and Structure.
+
+ The old schema assumes that every other dot implies an array. So a list
+ of two parameters,
+
+ [Integer("foo.bar.baz.quux"), Integer("foo.bar.shimmy")]
+
+ becomes::
+
+ {"foo": List(
+ item=Structure(
+ fields={"baz": List(item=Integer()),
+ "shimmy": Integer()}))}
+
+ By design, the old schema syntax ignored the names "bar" and "quux".
+ """
+ crap = {}
+ for parameter in parameters:
+ crap[parameter.name] = parameter
+ nest = self._convert_flat_to_nest(crap)
+ return self._secret_convert_old_schema(nest, 0).fields
+
+ def _secret_convert_old_schema(self, mapping, depth):
+ """
+ Internal recursion helper for L{_convert_old_schema}.
+ """
+ if not isinstance(mapping, dict):
+ return mapping
+ if depth % 2 == 0:
+ fields = {}
+ for k, v in mapping.iteritems():
+ fields[k] = self._secret_convert_old_schema(v, depth + 1)
+ return Structure(fields=fields)
+ else:
+ if not isinstance(mapping, dict):
+ raise TypeError("mapping %r must be a dict" % (mapping,))
+ if not len(mapping) == 1:
+ raise ValueError("mapping %r must only have one element"
+ % (mapping,))
+ item = mapping.values()[0]
+ item = self._secret_convert_old_schema(item, depth + 1)
+ return List(item=item)
=== modified file 'txaws/server/tests/test_schema.py'
--- txaws/server/tests/test_schema.py 2012-03-27 12:01:45 +0000
+++ txaws/server/tests/test_schema.py 2012-04-26 01:41:19 +0000
@@ -8,7 +8,9 @@
from txaws.server.exception import APIError
from txaws.server.schema import (
- Arguments, Bool, Date, Enum, Integer, Parameter, RawStr, Schema, Unicode)
+ Arguments, Bool, Date, Enum, Integer, Parameter, RawStr, Schema, Unicode,
+ List, Structure,
+ InconsistentParameterError, InvalidParameterValueError)
class ArgumentsTestCase(TestCase):
@@ -395,6 +397,26 @@
self.assertEqual(u"value", arguments.name)
self.assertEqual(None, arguments.count)
+ def test_extract_with_optional_default(self):
+ """
+ The value of C{default} on a parameter is used as the value when it is
+ not provided as an argument and the parameter is C{optional}.
+ """
+ schema = Schema(Unicode("name"),
+ Integer("count", optional=True, default=5))
+ arguments, _ = schema.extract({"name": "value"})
+ self.assertEqual(u"value", arguments.name)
+ self.assertEqual(5, arguments.count)
+
+ def test_extract_structure_with_optional(self):
+ """L{Schema.extract} can handle optional parameters."""
+ schema = Schema(
+ Structure(
+ "struct",
+ fields={"name": Unicode(optional=True, default="radix")}))
+ arguments, _ = schema.extract({"struct": {}})
+ self.assertEqual(u"radix", arguments.struct.name)
+
def test_extract_with_numbered(self):
"""
L{Schema.extract} can handle parameters with numbered values.
@@ -404,6 +426,16 @@
self.assertEqual("Joe", arguments.name[0])
self.assertEqual("Tom", arguments.name[1])
+ def test_extract_with_goofy_numbered(self):
+ """
+ L{Schema.extract} only uses the relative values of indices to determine
+ the index in the resultant list.
+ """
+ schema = Schema(Unicode("name.n"))
+ arguments, _ = schema.extract({"name.5": "Joe", "name.10": "Tom"})
+ self.assertEqual("Joe", arguments.name[0])
+ self.assertEqual("Tom", arguments.name[1])
+
def test_extract_with_single_numbered(self):
"""
L{Schema.extract} can handle a single parameter with a numbered value.
@@ -458,8 +490,8 @@
given without an index.
"""
schema = Schema(Unicode("name.n"))
- _, rest = schema.extract({"name": "foo", "name.1": "bar"})
- self.assertEqual(rest, {"name": "foo"})
+ self.assertRaises(InconsistentParameterError,
+ schema.extract, {"name": "foo", "name.1": "bar"})
def test_extract_with_non_numbered_template(self):
"""
@@ -480,7 +512,7 @@
error = self.assertRaises(APIError, schema.extract, params)
self.assertEqual(400, error.status)
self.assertEqual("UnknownParameter", error.code)
- self.assertEqual("The parameter name.one is not recognized",
+ self.assertEqual("The parameter one is not recognized",
error.message)
def test_extract_with_negative_index(self):
@@ -493,7 +525,7 @@
error = self.assertRaises(APIError, schema.extract, params)
self.assertEqual(400, error.status)
self.assertEqual("UnknownParameter", error.code)
- self.assertEqual("The parameter name.-1 is not recognized",
+ self.assertEqual("The parameter -1 is not recognized",
error.message)
def test_bundle(self):
@@ -524,7 +556,7 @@
L{Schema.bundle} correctly handles an empty numbered arguments list.
"""
schema = Schema(Unicode("name.n"))
- params = schema.bundle(names=[])
+ params = schema.bundle(name=[])
self.assertEqual({}, params)
def test_bundle_with_numbered_not_supplied(self):
@@ -544,6 +576,41 @@
self.assertEqual({"name.1": "Foo", "name.2": "Bar", "count": "123"},
params)
+ def test_bundle_with_structure(self):
+ """L{Schema.bundle} can bundle L{Structure}s."""
+ schema = Schema(
+ parameters={
+ "struct": Structure(fields={"field1": Unicode(),
+ "field2": Integer()})})
+ params = schema.bundle(struct={"field1": "hi", "field2": 59})
+ self.assertEqual({"struct.field1": "hi", "struct.field2": "59"},
+ params)
+
+ def test_bundle_with_list(self):
+ """L{Schema.bundle} can bundle L{List}s."""
+ schema = Schema(parameters={"things": List(item=Unicode())})
+ params = schema.bundle(things=["foo", "bar"])
+ self.assertEqual({"things.1": "foo", "things.2": "bar"}, params)
+
+ def test_bundle_with_structure_with_arguments(self):
+ """
+ L{Schema.bundle} can bundle L{Structure}s (specified as L{Arguments}).
+ """
+ schema = Schema(
+ parameters={
+ "struct": Structure(fields={"field1": Unicode(),
+ "field2": Integer()})})
+ params = schema.bundle(struct=Arguments({"field1": "hi",
+ "field2": 59}))
+ self.assertEqual({"struct.field1": "hi", "struct.field2": "59"},
+ params)
+
+ def test_bundle_with_list_with_arguments(self):
+ """L{Schema.bundle} can bundle L{List}s (specified as L{Arguments})."""
+ schema = Schema(parameters={"things": List(item=Unicode())})
+ params = schema.bundle(things=Arguments({1: "foo", 2: "bar"}))
+ self.assertEqual({"things.1": "foo", "things.2": "bar"}, params)
+
def test_bundle_with_arguments(self):
"""L{Schema.bundle} can bundle L{Arguments} too."""
schema = Schema(Unicode("name.n"), Integer("count"))
@@ -590,3 +657,77 @@
self.assertEqual(u"value", arguments.name)
self.assertEqual("testing", arguments.computer)
self.assertEqual(5, arguments.count)
+
+ def test_list(self):
+ """L{List}s can be extracted."""
+ schema = Schema(List("foo", Integer()))
+ arguments, _ = schema.extract({"foo.1": "1", "foo.2": "2"})
+ self.assertEqual([1, 2], arguments.foo)
+
+ def test_non_list(self):
+ """
+ When a non-list argument is passed to a L{List} parameter, a
+ L{InvalidParameterValueError} is raised.
+ """
+ schema = Schema(List("name", Unicode()))
+ self.assertRaises(InvalidParameterValueError,
+ schema.extract, {"name": "foo"})
+
+ def test_list_of_list(self):
+ """L{List}s can be nested."""
+ schema = Schema(List("foo", List(item=Unicode())))
+ arguments, _ = schema.extract(
+ {"foo.1.1": "first-first", "foo.1.2": "first-second",
+ "foo.2.1": "second-first", "foo.2.2": "second-second"})
+ self.assertEqual([["first-first", "first-second"],
+ ["second-first", "second-second"]],
+ arguments.foo)
+
+ def test_structure(self):
+ """
+ L{Schema}s with L{Structure} parameters can have arguments extracted.
+ """
+ schema = Schema(Structure("foo", {"a": Integer(), "b": Integer()}))
+ arguments, _ = schema.extract({"foo.a": "1", "foo.b": "2"})
+ self.assertEqual(1, arguments.foo.a)
+ self.assertEqual(2, arguments.foo.b)
+
+ def test_structure_of_structures(self):
+ """L{Structure}s can be nested."""
+ sub_struct = Structure(fields={"a": Unicode(), "b": Unicode()})
+ schema = Schema(Structure("foo", fields={"a": sub_struct,
+ "b": sub_struct}))
+ arguments, _ = schema.extract({"foo.a.a": "a-a", "foo.a.b": "a-b",
+ "foo.b.a": "b-a", "foo.b.b": "b-b"})
+ self.assertEqual("a-a", arguments.foo.a.a)
+ self.assertEqual("a-b", arguments.foo.a.b)
+ self.assertEqual("b-a", arguments.foo.b.a)
+ self.assertEqual("b-b", arguments.foo.b.b)
+
+ def test_list_of_structures(self):
+ """L{List}s of L{Structure}s are extracted properly."""
+ schema = Schema(
+ List("foo", Structure(fields={"a": Integer(), "b": Integer()})))
+ arguments, _ = schema.extract({"foo.1.a": "1", "foo.1.b": "2",
+ "foo.2.a": "3", "foo.2.b": "4"})
+ self.assertEqual(1, arguments.foo[0]['a'])
+ self.assertEqual(2, arguments.foo[0]['b'])
+ self.assertEqual(3, arguments.foo[1]['a'])
+ self.assertEqual(4, arguments.foo[1]['b'])
+
+ def test_structure_of_list(self):
+ """L{Structure}s of L{List}s are extracted properly."""
+ schema = Schema(Structure("foo", fields={"l": List(item=Integer())}))
+ arguments, _ = schema.extract({"foo.l.1": "1", "foo.l.2": "2"})
+ self.assertEqual([1, 2], arguments.foo.l)
+
+ def test_new_parameters(self):
+ """
+ L{Schema} accepts a C{parameters} parameter to specify parameters in a
+ {name: field} format.
+ """
+ schema = Schema(
+ parameters={"foo": Structure(
+ fields={"l": List(item=Integer())})})
+ arguments, _ = schema.extract({"foo.l.1": "1", "foo.l.2": "2"})
+ self.assertEqual([1, 2], arguments.foo.l)
Follow ups