Merge lp:~radix/txaws/ordered-parameters into lp:txaws

Proposed by Christopher Armstrong
Status: Merged
Approved by: Thomas Herve
Approved revision: 153
Merged at revision: 149
Proposed branch: lp:~radix/txaws/ordered-parameters
Merge into: lp:txaws
Diff against target: 500 lines (+221/-59)
4 files modified
txaws/server/registry.py (+12/-0)
txaws/server/schema.py (+115/-32)
txaws/server/tests/test_registry.py (+23/-0)
txaws/server/tests/test_schema.py (+71/-27)
To merge this branch: bzr merge lp:~radix/txaws/ordered-parameters
Reviewer Review Type Date Requested Status
Free Ekanayaka (community) Approve
Thomas Herve Approve
Review via email: mp+106933@code.launchpad.net

Description of the change

This branch adds the following methods:

- Schema.get_parameters
- MethodRegistry.get_action
- MethodRegistry.get_versions

It also goes to great lengths (gulp) to maintain the original order of parameters as passed to Schema() (both as positional arguments and when passed in the parameters= keyword argument).

Again, I'll be assigning the review to landscape for convenience, but if anyone else wants to review this branch, please feel free.

To post a comment you must log in.
Revision history for this message
Thomas Herve (therve) wrote :

Looks good to me, +1!

review: Approve
Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote :

Nice work, I have only minor comments, +1!

[1]

+ @param parameters: (keyword) The parameters of the API, as a either a

s/as a either/as either/

[2]

+ list of named L{Parameter} instances, or a mapping of parameter
+ names to L{Parameter} instances.

Are we retaining the old behavior for backward-compatibility? Eventually I believe it'd be desirable to stick to one form only. It's not really a problem, just something could be worth doing mid-term.

[3]

- def _inner_convert_old_schema(self, mapping, depth):
+ def _inner_convert_old_schema(self, mapping, depth, name):
         """
         Internal recursion helper for L{_convert_old_schema}.
         """
- if not isinstance(mapping, dict):
+ if not isinstance(mapping, list):

It took me a while to understand what the "mapping" parameter is. Firstly because "mapping" makes one thing to a dict, secondly because the term "mapping" is very generic and doesn't give any hint about what it actually maps.

Afaics "mapping" is a tree structure represented by an associative array, that is been traversed recursively, so maybe the term "node" would be more appropriate, e.g.:

def _inner_convert_old_schema(self, node_name, node_value, node_depth):
    if not isinstance(node_value, list):
        return node
    ....

[4]

+def _merge_alist(alist, path, value):

Calling it _merge_associative_list would be more verbose, but hopefully more clear for a casual reader of the code ("alist" could be mistaken for "a list", as some list).

review: Approve
Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote :

About [3], there's a typo: s/node/node_value/, and maybe a comment would be nice too:

def _inner_convert_old_schema(self, node_name, node_value, node_depth):
    if not isinstance(node_value, list):
        # This is a leaf node, we have a plain parameter that doesn't
        # need any conversion
        return node_value

lp:~radix/txaws/ordered-parameters updated
153. By Christopher Armstrong

Address all of free's comments [1-4].
- remove support for parameters={...}
- document schema conversion a lot more

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'txaws/server/registry.py'
2--- txaws/server/registry.py 2012-03-18 14:15:01 +0000
3+++ txaws/server/registry.py 2012-05-24 16:02:20 +0000
4@@ -52,3 +52,15 @@
5 # Only pass it if specified, for backward compatibility
6 kwargs["ignore"] = ignore
7 scanner.scan(module, **kwargs)
8+
9+ def get_actions(self):
10+ """
11+ Get a list of all action names.
12+ """
13+ return self._by_action.keys()
14+
15+ def get_versions(self, action_name):
16+ """
17+ Get a list of versions supported by the given action name.
18+ """
19+ return self._by_action[action_name].keys()
20
21=== modified file 'txaws/server/schema.py'
22--- txaws/server/schema.py 2012-05-04 17:46:22 +0000
23+++ txaws/server/schema.py 2012-05-24 16:02:20 +0000
24@@ -384,6 +384,7 @@
25 raise TypeError("Must provide fields")
26 super(Structure, self).__init__(name, optional=optional,
27 default=default, doc=doc)
28+ _namify_arguments(fields)
29 self.fields = fields
30
31 def parse(self, value):
32@@ -471,6 +472,18 @@
33 return value
34
35
36+def _namify_arguments(mapping):
37+ """
38+ Ensure that a mapping of names to parameters has the parameters set to the
39+ correct name.
40+ """
41+ result = []
42+ for name, parameter in mapping.iteritems():
43+ parameter.name = name
44+ result.append(parameter)
45+ return result
46+
47+
48 class Schema(object):
49 """
50 The schema that the arguments of an HTTP request must be compliant with.
51@@ -482,7 +495,7 @@
52 Any number of L{Parameter} instances can be passed. The parameter names
53 are used in L{Schema.extract} and L{Schema.bundle}. For example::
54
55- schema = Schema(name="SetName", parameters={"Name": Unicode()})
56+ schema = Schema(name="SetName", parameters=[Unicode("Name")])
57
58 means that the result of L{Schema.extract} would have a C{Name}
59 attribute. Similarly, L{Schema.bundle} would look for a C{Name}
60@@ -492,7 +505,7 @@
61
62 schema = Schema(
63 name="SetNames",
64- parameters={"Names": List(item=Unicode())})
65+ parameters=[List("Names", Unicode())])
66
67 means that the result of L{Schema.extract} would have a C{Names}
68 attribute, which would itself contain a list of names. Similarly,
69@@ -504,13 +517,12 @@
70
71 @param name: (keyword) The name of the API call that this schema
72 represents. Accessible via the C{name} attribute.
73- @param parameters: (keyword) The parameters of the API, as a mapping
74- of parameter names to L{Parameter} instances.
75+ @param parameters: (keyword) The parameters of the API, as a list of
76+ named L{Parameter} instances.
77 @param doc: (keyword) The documentation of this API Call. Accessible
78 via the C{doc} attribute.
79- @param result: (keyword) A description of the result of this API call,
80- in the same format as C{parameters}. Accessible via the C{result}
81- attribute.
82+ @param result: (keyword) A description of the result of this API
83+ call. Accessible via the C{result} attribute.
84 @param errors: (keyword) A sequence of exception classes that the API
85 can potentially raise. Accessible as a L{set} via the C{errors}
86 attribute.
87@@ -527,6 +539,12 @@
88 else:
89 self._parameters = self._convert_old_schema(_parameters)
90
91+ def get_parameters(self):
92+ """
93+ Get the list of parameters this schema supports.
94+ """
95+ return self._parameters[:]
96+
97 def extract(self, params):
98 """Extract parameters from a raw C{dict} according to this schema.
99
100@@ -534,7 +552,8 @@
101 @return: A tuple of an L{Arguments} object holding the extracted
102 arguments and any unparsed arguments.
103 """
104- structure = Structure(fields=self._parameters)
105+ structure = Structure(fields=dict([(p.name, p)
106+ for p in self._parameters]))
107 try:
108 tree = structure.coerce(self._convert_flat_to_nest(params))
109 rest = {}
110@@ -563,7 +582,7 @@
111 continue
112 segments = name.split('.')
113 first = segments[0]
114- parameter = self._parameters.get(first)
115+ parameter = self.get_parameter(first)
116 if parameter is None:
117 raise RuntimeError("Parameter '%s' not in schema" % name)
118 else:
119@@ -574,6 +593,14 @@
120
121 return self._convert_nest_to_flat(result)
122
123+ def get_parameter(self, name):
124+ """
125+ Get the parameter on this schema with the given C{name}.
126+ """
127+ for parameter in self._parameters:
128+ if parameter.name == name:
129+ return parameter
130+
131 def _convert_flat_to_nest(self, params):
132 """
133 Convert a structure in the form of::
134@@ -647,17 +674,19 @@
135 new_kwargs = {
136 'name': self.name,
137 'doc': self.doc,
138- 'parameters': self._parameters.copy(),
139+ 'parameters': self._parameters[:],
140 'result': self.result.copy() if self.result else {},
141 'errors': self.errors.copy() if self.errors else set()}
142- new_kwargs['parameters'].update(kwargs.pop('parameters', {}))
143+ if 'parameters' in kwargs:
144+ new_params = kwargs.pop('parameters')
145+ new_kwargs['parameters'].extend(new_params)
146 new_kwargs['result'].update(kwargs.pop('result', {}))
147 new_kwargs['errors'].update(kwargs.pop('errors', set()))
148 new_kwargs.update(kwargs)
149
150 if schema_items:
151 parameters = self._convert_old_schema(schema_items)
152- new_kwargs['parameters'].update(parameters)
153+ new_kwargs['parameters'].extend(parameters)
154 return Schema(**new_kwargs)
155
156 def _convert_old_schema(self, parameters):
157@@ -672,37 +701,91 @@
158
159 becomes::
160
161- {"foo": List(
162+ [List(
163+ "foo",
164 item=Structure(
165 fields={"baz": List(item=Integer()),
166- "shimmy": Integer()}))}
167+ "shimmy": Integer()}))]
168
169 By design, the old schema syntax ignored the names "bar" and "quux".
170 """
171- crap = {}
172+ # 'merged' here is an associative list that maps parameter names to
173+ # Parameter instances, OR sub-associative lists which represent nested
174+ # lists and structures.
175+ # e.g.,
176+ # [Integer("foo")]
177+ # becomes
178+ # [("foo", Integer("foo"))]
179+ # and
180+ # [Integer("foo.bar")]
181+ # (which represents a list of integers called "foo" with a meaningless
182+ # index name of "bar") becomes
183+ # [("foo", [("bar", Integer("foo.bar"))])].
184+ merged = []
185 for parameter in parameters:
186- crap[parameter.name] = parameter
187- nest = self._convert_flat_to_nest(crap)
188- return self._inner_convert_old_schema(nest, 0).fields
189+ segments = parameter.name.split('.')
190+ _merge_associative_list(merged, segments, parameter)
191+ result = [self._inner_convert_old_schema(node, 1) for node in merged]
192+ return result
193
194- def _inner_convert_old_schema(self, mapping, depth):
195+ def _inner_convert_old_schema(self, node, depth):
196 """
197 Internal recursion helper for L{_convert_old_schema}.
198+
199+ @param node: A node in the associative list tree as described in
200+ _convert_old_schema. A two tuple of (name, parameter).
201+ @param depth: The depth that the node is at. This is important to know
202+ if we're currently processing a list or a structure. ("foo.N" is a
203+ list called "foo", "foo.N.fieldname" describes a field in a list of
204+ structs).
205 """
206- if not isinstance(mapping, dict):
207- return mapping
208+ name, parameter_description = node
209+ if not isinstance(parameter_description, list):
210+ # This is a leaf, i.e., an actual L{Parameter} instance.
211+ return parameter_description
212 if depth % 2 == 0:
213+ # we're processing a structure.
214 fields = {}
215- for k, v in mapping.iteritems():
216- fields[k] = self._inner_convert_old_schema(v, depth + 1)
217- return Structure("anonymous_structure", fields=fields)
218+ for node in parameter_description:
219+ fields[node[0]] = self._inner_convert_old_schema(node, depth + 1)
220+ return Structure(name, fields=fields)
221 else:
222- if not isinstance(mapping, dict):
223- raise TypeError("mapping %r must be a dict" % (mapping,))
224- if not len(mapping) == 1:
225- raise ValueError("Multiple different index names specified: %r"
226- % (mapping.keys(),))
227- item = mapping.values()[0]
228- item = self._inner_convert_old_schema(item, depth + 1)
229- name = item.name.split('.', 1)[0]
230+ # we're processing a list.
231+ if not isinstance(parameter_description, list):
232+ raise TypeError("node %r must be an associative list"
233+ % (parameter_description,))
234+ if not len(parameter_description) == 1:
235+ raise ValueError(
236+ "Multiple different index names specified: %r"
237+ % ([item[0] for item in parameter_description],))
238+ subnode = parameter_description[0]
239+ item = self._inner_convert_old_schema(subnode, depth + 1)
240 return List(name=name, item=item, optional=item.optional)
241+
242+
243+def _merge_associative_list(alist, path, value):
244+ """
245+ Merge a value into an associative list at the given path, maintaining
246+ insertion order. Examples will explain it::
247+
248+ >>> alist = []
249+ >>> _merge_associative_list(alist, ["foo", "bar"], "barvalue")
250+ >>> _merge_associative_list(alist, ["foo", "baz"], "bazvalue")
251+ >>> alist == [("foo", [("bar", "barvalue"), ("baz", "bazvalue")])]
252+
253+ @param alist: An associative list of names to values.
254+ @param path: A path through sub-alists which we ultimately want to point to
255+ C{value}.
256+ @param value: The value to set.
257+ @return: None. This operation mutates the associative list in place.
258+ """
259+ for key in path[:-1]:
260+ for item in alist:
261+ if item[0] == key:
262+ alist = item[1]
263+ break
264+ else:
265+ subalist = []
266+ alist.append((key, subalist))
267+ alist = subalist
268+ alist.append((path[-1], value))
269
270=== modified file 'txaws/server/tests/test_registry.py'
271--- txaws/server/tests/test_registry.py 2012-03-18 14:15:01 +0000
272+++ txaws/server/tests/test_registry.py 2012-05-24 16:02:20 +0000
273@@ -51,6 +51,29 @@
274 self.assertRaises(RuntimeError, self.registry.add, TestMethod2,
275 "test", "1.0")
276
277+ def test_get_actions(self):
278+ """
279+ L{MethodRegistry.get_actions} returns a list of action names.
280+ """
281+ self.registry.add(TestMethod, "test", "1.0")
282+ self.registry.add(TestMethod, "test", "2.0")
283+ self.registry.add(TestMethod, "test2", "1.0")
284+ self.assertEqual(["test", "test2"],
285+ sorted(self.registry.get_actions()))
286+
287+ def test_get_versions(self):
288+ """
289+ L{MethodRegistry.get_versions} returns a list of versions supported by
290+ the action.
291+ """
292+ self.registry.add(TestMethod, "test", "1.0")
293+ self.registry.add(TestMethod, "test", "2.0")
294+ self.registry.add(TestMethod, "test2", "1.0")
295+ self.assertEqual(["1.0", "2.0"],
296+ sorted(self.registry.get_versions("test")))
297+ self.assertEqual(["1.0"],
298+ sorted(self.registry.get_versions("test2")))
299+
300 def test_get(self):
301 """
302 L{MethodRegistry.get} returns the method class registered for the
303
304=== modified file 'txaws/server/tests/test_schema.py'
305--- txaws/server/tests/test_schema.py 2012-05-04 17:48:57 +0000
306+++ txaws/server/tests/test_schema.py 2012-05-24 16:02:20 +0000
307@@ -384,6 +384,29 @@
308
309 class SchemaTestCase(TestCase):
310
311+ def test_get_parameters(self):
312+ """
313+ L{Schema.get_parameters} returns the original list of parameters.
314+ """
315+ schema = Schema(parameters=[
316+ Unicode("name"),
317+ List("scores", Integer())])
318+ parameters = schema.get_parameters()
319+ self.assertEqual("name", parameters[0].name)
320+ self.assertEqual("scores", parameters[1].name)
321+
322+ def test_get_parameters_order_on_parameter_only_construction(self):
323+ """
324+ L{Schema.get_parameters} returns the original list of L{Parameter}s
325+ even when they are passed as positional arguments to L{Schema}.
326+ """
327+ schema = Schema(
328+ Unicode("name"),
329+ List("scores", Integer()),
330+ Integer("index", Integer()))
331+ self.assertEqual(["name", "scores", "index"],
332+ [p.name for p in schema.get_parameters()])
333+
334 def test_extract(self):
335 """
336 L{Schema.extract} returns an L{Argument} object whose attributes are
337@@ -516,7 +539,7 @@
338 """
339 schema = Schema(Unicode("name.n"))
340 self.assertRaises(InconsistentParameterError,
341- schema.extract, {"name": "foo", "name.1": "bar"})
342+ schema.extract, {"nameFOOO": "foo", "nameFOOO.1": "bar"})
343
344 def test_extract_with_non_numbered_template(self):
345 """
346@@ -614,16 +637,16 @@
347 def test_bundle_with_structure(self):
348 """L{Schema.bundle} can bundle L{Structure}s."""
349 schema = Schema(
350- parameters={
351- "struct": Structure(fields={"field1": Unicode(),
352- "field2": Integer()})})
353+ parameters=[
354+ Structure("struct", fields={"field1": Unicode(),
355+ "field2": Integer()})])
356 params = schema.bundle(struct={"field1": "hi", "field2": 59})
357 self.assertEqual({"struct.field1": "hi", "struct.field2": "59"},
358 params)
359
360 def test_bundle_with_list(self):
361 """L{Schema.bundle} can bundle L{List}s."""
362- schema = Schema(parameters={"things": List(item=Unicode())})
363+ schema = Schema(parameters=[List("things", item=Unicode())])
364 params = schema.bundle(things=["foo", "bar"])
365 self.assertEqual({"things.1": "foo", "things.2": "bar"}, params)
366
367@@ -632,9 +655,9 @@
368 L{Schema.bundle} can bundle L{Structure}s (specified as L{Arguments}).
369 """
370 schema = Schema(
371- parameters={
372- "struct": Structure(fields={"field1": Unicode(),
373- "field2": Integer()})})
374+ parameters=[
375+ Structure("struct", fields={"field1": Unicode(),
376+ "field2": Integer()})])
377 params = schema.bundle(struct=Arguments({"field1": "hi",
378 "field2": 59}))
379 self.assertEqual({"struct.field1": "hi", "struct.field2": "59"},
380@@ -642,7 +665,7 @@
381
382 def test_bundle_with_list_with_arguments(self):
383 """L{Schema.bundle} can bundle L{List}s (specified as L{Arguments})."""
384- schema = Schema(parameters={"things": List(item=Unicode())})
385+ schema = Schema(parameters=[List("things", item=Unicode())])
386 params = schema.bundle(things=Arguments({1: "foo", 2: "bar"}))
387 self.assertEqual({"things.1": "foo", "things.2": "bar"}, params)
388
389@@ -770,17 +793,20 @@
390 {name: field} format.
391 """
392 schema = Schema(
393- parameters={"foo": Structure(
394- fields={"l": List(item=Integer())})})
395+ parameters=[Structure("foo",
396+ fields={"l": List(item=Integer())})])
397 arguments, _ = schema.extract({"foo.l.1": "1", "foo.l.2": "2"})
398 self.assertEqual([1, 2], arguments.foo.l)
399
400- def test_schema_conversion_list_name(self):
401+ def test_schema_conversion_list(self):
402 """
403 Backwards-compatibility conversion maintains the name of lists.
404 """
405 schema = Schema(Unicode("foos.N"))
406- self.assertEqual("foos", schema._parameters["foos"].name)
407+ parameters = schema.get_parameters()
408+ self.assertEqual(1, len(parameters))
409+ self.assertTrue(isinstance(parameters[0], List))
410+ self.assertEqual("foos", parameters[0].name)
411
412 def test_schema_conversion_structure_name(self):
413 """
414@@ -789,12 +815,16 @@
415 """
416 schema = Schema(Unicode("foos.N.field"),
417 Unicode("foos.N.field2"))
418- self.assertEqual("anonymous_structure",
419- schema._parameters["foos"].item.name)
420- self.assertEqual("foos.N.field",
421- schema._parameters["foos"].item.fields["field"].name)
422- self.assertEqual("foos.N.field2",
423- schema._parameters["foos"].item.fields["field2"].name)
424+ parameters = schema.get_parameters()
425+ self.assertEqual(1, len(parameters))
426+ self.assertTrue(isinstance(parameters[0], List))
427+ self.assertEqual("foos", parameters[0].name)
428+ self.assertEqual("N",
429+ parameters[0].item.name)
430+ self.assertEqual("field",
431+ parameters[0].item.fields["field"].name)
432+ self.assertEqual("field2",
433+ parameters[0].item.fields["field2"].name)
434
435 def test_schema_conversion_optional_list(self):
436 """
437@@ -829,9 +859,9 @@
438 schema = Schema(
439 name="GetStuff",
440 doc="""Get the stuff.""",
441- parameters={
442- 'id': Integer(),
443- 'scope': Unicode()},
444+ parameters=[
445+ Integer("id"),
446+ Unicode("scope")],
447 result=result,
448 errors=errors)
449
450@@ -852,12 +882,12 @@
451
452 schema = Schema(
453 name="GetStuff",
454- parameters={"id": Integer()})
455+ parameters=[Integer("id")])
456
457 schema2 = schema.extend(
458 name="GetStuff2",
459 doc="Get stuff 2",
460- parameters={'scope': Unicode()},
461+ parameters=[Unicode("scope")],
462 result=result,
463 errors=errors)
464
465@@ -884,11 +914,11 @@
466 schema = Schema(
467 name="GetStuff",
468 doc="""Get the stuff.""",
469- parameters={'id': Integer()},
470+ parameters=[Integer("id")],
471 result=result,
472 errors=errors)
473
474- schema2 = schema.extend(parameters={'scope': Unicode()})
475+ schema2 = schema.extend(parameters=[Unicode("scope")])
476
477 self.assertEqual("GetStuff", schema2.name)
478 self.assertEqual("Get the stuff.", schema2.doc)
479@@ -917,6 +947,20 @@
480 """
481 Errors can be extended with L{Schema.extend}.
482 """
483- schema = Schema(parameters={}, errors=[APIError])
484+ schema = Schema(parameters=[], errors=[APIError])
485 schema2 = schema.extend(errors=[ZeroDivisionError])
486 self.assertEqual(set([APIError, ZeroDivisionError]), schema2.errors)
487+
488+ def test_extend_maintains_parameter_order(self):
489+ """
490+ Extending a schema with additional parameters puts the new parameters
491+ at the end.
492+ """
493+ schema = Schema(parameters=[Unicode("name"), Unicode("value")])
494+ schema2 = schema.extend(parameters=[Integer("foo"), Unicode("index")])
495+ self.assertEqual(["name", "value", "foo", "index"],
496+ [p.name for p in schema2.get_parameters()])
497+
498+ def test_schema_field_names(self):
499+ structure = Structure(fields={"foo": Integer()})
500+ self.assertEqual("foo", structure.fields["foo"].name)

Subscribers

People subscribed via source and target branches