Merge lp:~radix/txaws/ordered-parameters into lp:txaws
- ordered-parameters
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Free Ekanayaka (community) | Approve | ||
Thomas Herve | Approve | ||
Review via email: mp+106933@code.launchpad.net |
Commit message
Description of the change
This branch adds the following methods:
- Schema.
- MethodRegistry.
- MethodRegistry.
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.
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-
[3]
- def _inner_
+ def _inner_
"""
Internal recursion helper for L{_convert_
"""
- 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_
if not isinstance(
return node
....
[4]
+def _merge_alist(alist, path, value):
Calling it _merge_
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_
if not isinstance(
# This is a leaf node, we have a plain parameter that doesn't
# need any conversion
return node_value
- 153. By Christopher Armstrong
-
Address all of free's comments [1-4].
- remove support for parameters={...}
- document schema conversion a lot more
Preview Diff
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) |
Looks good to me, +1!