Merge lp:~leonardr/lazr.restful/use-bleedthrough-for-methods into lp:lazr.restful
- use-bleedthrough-for-methods
- Merge into trunk
Proposed by
Leonard Richardson
Status: | Merged |
---|---|
Merged at revision: | not available |
Proposed branch: | lp:~leonardr/lazr.restful/use-bleedthrough-for-methods |
Merge into: | lp:lazr.restful |
Diff against target: |
244 lines (+132/-10) 4 files modified
src/lazr/restful/declarations.py (+36/-2) src/lazr/restful/docs/utils.txt (+3/-0) src/lazr/restful/docs/webservice-declarations.txt (+82/-4) src/lazr/restful/utils.py (+11/-4) |
To merge this branch: | bzr merge lp:~leonardr/lazr.restful/use-bleedthrough-for-methods |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Brad Crittenden (community) | code | Approve | |
Review via email: mp+17683@code.launchpad.net |
Commit message
Description of the change
To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote : | # |
Revision history for this message
Brad Crittenden (bac) wrote : | # |
Very nice branch Leonard. On IRC I suggested adding a comment about the use of None on the stack and you proposed a nice solution. With that change it looks great.
review:
Approve
(code)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'src/lazr/restful/declarations.py' | |||
2 | --- src/lazr/restful/declarations.py 2010-01-11 18:27:43 +0000 | |||
3 | +++ src/lazr/restful/declarations.py 2010-01-19 20:04:14 +0000 | |||
4 | @@ -27,6 +27,7 @@ | |||
5 | 27 | 'generate_entry_interface', | 27 | 'generate_entry_interface', |
6 | 28 | 'generate_operation_adapter', | 28 | 'generate_operation_adapter', |
7 | 29 | 'mutator_for', | 29 | 'mutator_for', |
8 | 30 | 'operation_for_version', | ||
9 | 30 | 'operation_parameters', | 31 | 'operation_parameters', |
10 | 31 | 'operation_returns_entry', | 32 | 'operation_returns_entry', |
11 | 32 | 'operation_returns_collection_of', | 33 | 'operation_returns_collection_of', |
12 | @@ -56,7 +57,9 @@ | |||
13 | 56 | from lazr.restful import ( | 57 | from lazr.restful import ( |
14 | 57 | Collection, Entry, EntryAdapterUtility, ResourceOperation, ObjectLink) | 58 | Collection, Entry, EntryAdapterUtility, ResourceOperation, ObjectLink) |
15 | 58 | from lazr.restful.security import protect_schema | 59 | from lazr.restful.security import protect_schema |
17 | 59 | from lazr.restful.utils import camelcase_to_underscore_separated, get_current_browser_request | 60 | from lazr.restful.utils import ( |
18 | 61 | BleedThroughDict, camelcase_to_underscore_separated, | ||
19 | 62 | get_current_browser_request) | ||
20 | 60 | 63 | ||
21 | 61 | LAZR_WEBSERVICE_EXPORTED = '%s.exported' % LAZR_WEBSERVICE_NS | 64 | LAZR_WEBSERVICE_EXPORTED = '%s.exported' % LAZR_WEBSERVICE_NS |
22 | 62 | COLLECTION_TYPE = 'collection' | 65 | COLLECTION_TYPE = 'collection' |
23 | @@ -294,7 +297,17 @@ | |||
24 | 294 | """Annotates the function with the fixed arguments.""" | 297 | """Annotates the function with the fixed arguments.""" |
25 | 295 | # Everything in the function dictionary ends up as tagged value | 298 | # Everything in the function dictionary ends up as tagged value |
26 | 296 | # in the interface method specification. | 299 | # in the interface method specification. |
28 | 297 | annotations = method.__dict__.setdefault(LAZR_WEBSERVICE_EXPORTED, {}) | 300 | annotations = method.__dict__.get(LAZR_WEBSERVICE_EXPORTED, None) |
29 | 301 | if annotations is None: | ||
30 | 302 | # Create a new bleed-through dict which associates | ||
31 | 303 | # annotation data with the earliest active version of the | ||
32 | 304 | # web service. Future @webservice_version annotations will | ||
33 | 305 | # push later versions onto the BleedThroughDict, allowing | ||
34 | 306 | # new versions to specify annotation data that conflicts | ||
35 | 307 | # with old versions. | ||
36 | 308 | annotations = BleedThroughDict() | ||
37 | 309 | annotations.push(None) | ||
38 | 310 | method.__dict__[LAZR_WEBSERVICE_EXPORTED] = annotations | ||
39 | 298 | self.annotate_method(method, annotations) | 311 | self.annotate_method(method, annotations) |
40 | 299 | return method | 312 | return method |
41 | 300 | 313 | ||
42 | @@ -442,6 +455,27 @@ | |||
43 | 442 | set(annotations.get('call_with', {}).keys())) | 455 | set(annotations.get('call_with', {}).keys())) |
44 | 443 | 456 | ||
45 | 444 | 457 | ||
46 | 458 | class operation_for_version(_method_annotator): | ||
47 | 459 | """Decorator specifying which version of the webservice is defined. | ||
48 | 460 | |||
49 | 461 | Decorators processed after this one will decorate the given web | ||
50 | 462 | service version and, by default, subsequent versions will inherit | ||
51 | 463 | their values. Subsequent versions may provide conflicting values, | ||
52 | 464 | but those values will not affect this version. | ||
53 | 465 | """ | ||
54 | 466 | def __init__(self, version): | ||
55 | 467 | _check_called_from_interface_def('%s()' % self.__class__.__name__) | ||
56 | 468 | self.version = version | ||
57 | 469 | |||
58 | 470 | def annotate_method(self, method, annotations): | ||
59 | 471 | """See `_method_annotator`.""" | ||
60 | 472 | # The annotations dict is a BleedThroughDict. Push a new dict | ||
61 | 473 | # onto its stack, labeled with the version number, so that | ||
62 | 474 | # future annotations can override old annotations without | ||
63 | 475 | # destroying them. | ||
64 | 476 | annotations.push(self.version) | ||
65 | 477 | |||
66 | 478 | |||
67 | 445 | class export_operation_as(_method_annotator): | 479 | class export_operation_as(_method_annotator): |
68 | 446 | """Decorator specifying the name to export the method as.""" | 480 | """Decorator specifying the name to export the method as.""" |
69 | 447 | 481 | ||
70 | 448 | 482 | ||
71 | === modified file 'src/lazr/restful/docs/utils.txt' | |||
72 | --- src/lazr/restful/docs/utils.txt 2010-01-14 10:00:38 +0000 | |||
73 | +++ src/lazr/restful/docs/utils.txt 2010-01-19 20:04:14 +0000 | |||
74 | @@ -57,6 +57,9 @@ | |||
75 | 57 | >>> print stack.get('key', 'default') | 57 | >>> print stack.get('key', 'default') |
76 | 58 | value | 58 | value |
77 | 59 | 59 | ||
78 | 60 | >>> 'key' in stack | ||
79 | 61 | True | ||
80 | 62 | |||
81 | 60 | >>> sorted(stack.items()) | 63 | >>> sorted(stack.items()) |
82 | 61 | [('key', 'value')] | 64 | [('key', 'value')] |
83 | 62 | 65 | ||
84 | 63 | 66 | ||
85 | === modified file 'src/lazr/restful/docs/webservice-declarations.txt' | |||
86 | --- src/lazr/restful/docs/webservice-declarations.txt 2010-01-11 18:27:43 +0000 | |||
87 | +++ src/lazr/restful/docs/webservice-declarations.txt 2010-01-19 20:04:14 +0000 | |||
88 | @@ -267,11 +267,12 @@ | |||
89 | 267 | Exporting methods | 267 | Exporting methods |
90 | 268 | ================= | 268 | ================= |
91 | 269 | 269 | ||
95 | 270 | Entries and collections can support operations on the webservice. The | 270 | Entries and collections can publish named operations on the |
96 | 271 | operations supported are defined by tagging methods in the content | 271 | webservice. Every named operation corresponds to some method defined |
97 | 272 | interface with special decorators. | 272 | on the content interface. To publish a method as a named operation, |
98 | 273 | you tag it with special decorators. | ||
99 | 273 | 274 | ||
101 | 274 | Three different decorators are used based on the kind of method | 275 | Four different decorators are used based on the kind of method |
102 | 275 | exported. | 276 | exported. |
103 | 276 | 277 | ||
104 | 277 | 1. @export_read_operation | 278 | 1. @export_read_operation |
105 | @@ -295,6 +296,11 @@ | |||
106 | 295 | creating and the name of the fields in the schema that are passed as | 296 | creating and the name of the fields in the schema that are passed as |
107 | 296 | parameters. | 297 | parameters. |
108 | 297 | 298 | ||
109 | 299 | 4. @export_destructor_operation | ||
110 | 300 | |||
111 | 301 | This will mark the method as available as a DELETE operation on the | ||
112 | 302 | exported resource. | ||
113 | 303 | |||
114 | 298 | The specification of the web service's acceptable method parameters | 304 | The specification of the web service's acceptable method parameters |
115 | 299 | should be described using the @operation_parameters decorator, which | 305 | should be described using the @operation_parameters decorator, which |
116 | 300 | takes normal IField instances. | 306 | takes normal IField instances. |
117 | @@ -444,6 +450,9 @@ | |||
118 | 444 | return_type: <lazr.restful._operation.ObjectLink object...> | 450 | return_type: <lazr.restful._operation.ObjectLink object...> |
119 | 445 | type: 'factory' | 451 | type: 'factory' |
120 | 446 | 452 | ||
121 | 453 | Default values and required parameters | ||
122 | 454 | -------------------------------------- | ||
123 | 455 | |||
124 | 447 | Parameters default and required attributes are set automatically based | 456 | Parameters default and required attributes are set automatically based |
125 | 448 | on the method signature. | 457 | on the method signature. |
126 | 449 | 458 | ||
127 | @@ -494,6 +503,75 @@ | |||
128 | 494 | >>> param_defs['optional2'].default | 503 | >>> param_defs['optional2'].default |
129 | 495 | u'Default2' | 504 | u'Default2' |
130 | 496 | 505 | ||
131 | 506 | Versioning | ||
132 | 507 | ---------- | ||
133 | 508 | |||
134 | 509 | Different versions of the webservice can publish the same interface | ||
135 | 510 | method in totally different ways. Here's a simple example. This method | ||
136 | 511 | appears differently in three versions of the web service: 2.0, 1.0, | ||
137 | 512 | and in an unnamed pre-1.0 version. | ||
138 | 513 | |||
139 | 514 | >>> from lazr.restful.declarations import operation_for_version | ||
140 | 515 | >>> class MultiVersionMethod(Interface): | ||
141 | 516 | ... export_as_webservice_entry() | ||
142 | 517 | ... | ||
143 | 518 | ... @call_with(fixed='2.0 value') | ||
144 | 519 | ... @operation_for_version('2.0') | ||
145 | 520 | ... | ||
146 | 521 | ... @call_with(fixed='1.0 value') | ||
147 | 522 | ... @export_operation_as('new_name') | ||
148 | 523 | ... @operation_for_version('1.0') | ||
149 | 524 | ... | ||
150 | 525 | ... @call_with(fixed='pre-1.0 value') | ||
151 | 526 | ... @operation_parameters( | ||
152 | 527 | ... required=TextLine(), | ||
153 | 528 | ... fixed=TextLine() | ||
154 | 529 | ... ) | ||
155 | 530 | ... @export_read_operation() | ||
156 | 531 | ... def a_method(required, fixed='Fixed value'): | ||
157 | 532 | ... """Method demonstrating multiversion publication.""" | ||
158 | 533 | |||
159 | 534 | The tagged value containing the annotations looks like a dictionary, | ||
160 | 535 | but it's actually a stack of dictionaries named after the versions. | ||
161 | 536 | |||
162 | 537 | >>> dictionary = MultiVersionMethod['a_method'].getTaggedValue( | ||
163 | 538 | ... 'lazr.restful.exported') | ||
164 | 539 | >>> dictionary.dict_names | ||
165 | 540 | [None, '1.0', '2.0'] | ||
166 | 541 | |||
167 | 542 | The dictionary on top of the stack is for the 2.0 version of the web | ||
168 | 543 | service. In 2.0, the method is published as 'new_name' and its 'fixed' | ||
169 | 544 | argument is fixed to the string '2.0 value'. | ||
170 | 545 | |||
171 | 546 | >>> print dictionary['as'] | ||
172 | 547 | new_name | ||
173 | 548 | >>> dictionary['call_with'] | ||
174 | 549 | {'fixed': '2.0 value'} | ||
175 | 550 | |||
176 | 551 | Let's pop the 2.0 version off the stack. Now we can see how the method | ||
177 | 552 | looks in 1.0. It's still called 'new_name', but its 'fixed' argument | ||
178 | 553 | is fixed to the string '1.0 value'. | ||
179 | 554 | |||
180 | 555 | >>> ignored = dictionary.pop() | ||
181 | 556 | >>> print dictionary['as'] | ||
182 | 557 | new_name | ||
183 | 558 | >>> dictionary['call_with'] | ||
184 | 559 | {'fixed': '1.0 value'} | ||
185 | 560 | |||
186 | 561 | Let's pop one more time to see how the method looks in the pre-1.0 | ||
187 | 562 | version. It hasn't yet been renamed to 'new_name', and its 'fixed' | ||
188 | 563 | argument is fixed to the string 'pre-1.0 value'. | ||
189 | 564 | |||
190 | 565 | >>> ignored = dictionary.pop() | ||
191 | 566 | >>> print dictionary.get('as') | ||
192 | 567 | None | ||
193 | 568 | >>> dictionary['call_with'] | ||
194 | 569 | {'fixed': 'pre-1.0 value'} | ||
195 | 570 | >>> print dictionary.items() | ||
196 | 571 | |||
197 | 572 | Error handling | ||
198 | 573 | -------------- | ||
199 | 574 | |||
200 | 497 | All these decorators can only be used from within an interface | 575 | All these decorators can only be used from within an interface |
201 | 498 | definition: | 576 | definition: |
202 | 499 | 577 | ||
203 | 500 | 578 | ||
204 | === modified file 'src/lazr/restful/utils.py' | |||
205 | --- src/lazr/restful/utils.py 2010-01-14 17:08:20 +0000 | |||
206 | +++ src/lazr/restful/utils.py 2010-01-19 20:04:14 +0000 | |||
207 | @@ -41,9 +41,11 @@ | |||
208 | 41 | """ | 41 | """ |
209 | 42 | missing = object() | 42 | missing = object() |
210 | 43 | 43 | ||
212 | 44 | def __init__(self): | 44 | def __init__(self, name=None, dictionary=None, opaque=False): |
213 | 45 | """Initialize the bleed-through dictionary.""" | 45 | """Initialize the bleed-through dictionary.""" |
214 | 46 | self.stack = [] | 46 | self.stack = [] |
215 | 47 | if name is not None and dictionary is not None: | ||
216 | 48 | self.push(name, dictionary, opaque) | ||
217 | 47 | 49 | ||
218 | 48 | def push(self, name, dictionary=None, opaque=False): | 50 | def push(self, name, dictionary=None, opaque=False): |
219 | 49 | """Pushes a dictionary onto the stack. | 51 | """Pushes a dictionary onto the stack. |
220 | @@ -53,9 +55,6 @@ | |||
221 | 53 | :arg opaque: If True, values will not 'bleed through' from | 55 | :arg opaque: If True, values will not 'bleed through' from |
222 | 54 | dictionaries lower on the stack. | 56 | dictionaries lower on the stack. |
223 | 55 | """ | 57 | """ |
224 | 56 | if name is None: | ||
225 | 57 | self.stack.append(name) | ||
226 | 58 | return | ||
227 | 59 | if dictionary is None: | 58 | if dictionary is None: |
228 | 60 | dictionary = {} | 59 | dictionary = {} |
229 | 61 | self.stack.append((name, dict(dictionary), opaque)) | 60 | self.stack.append((name, dict(dictionary), opaque)) |
230 | @@ -83,6 +82,14 @@ | |||
231 | 83 | """Is the stack empty?""" | 82 | """Is the stack empty?""" |
232 | 84 | return len(self.stack) == 0 | 83 | return len(self.stack) == 0 |
233 | 85 | 84 | ||
234 | 85 | def setdefault(self, key, value): | ||
235 | 86 | """Get a from the top of the stack, setting it if not present.""" | ||
236 | 87 | return self.stack[0][1].setdefault(key, value) | ||
237 | 88 | |||
238 | 89 | def __contains__(self, key): | ||
239 | 90 | """Check whether a key is visible in the stack.""" | ||
240 | 91 | return self.get(key, missing) is not missing | ||
241 | 92 | |||
242 | 86 | def __getitem__(self, key): | 93 | def __getitem__(self, key): |
243 | 87 | """Look up an item somewhere in the stack.""" | 94 | """Look up an item somewhere in the stack.""" |
244 | 88 | if self.is_empty: | 95 | if self.is_empty: |
This branch creates the 'operation_ for_version' annotation, used to publish a named operation in different ways for different versions of the web service. Currently 'operation_ for_version' lets you define different versions of the methods, but it doesn't really matter, because the bleed-through stack means that only the most recent version is visible.
I made some changes to BleedThroughDict, implementing additional bits of the dict interface, so that the code in declarations.py could treat a BTD like a normal dict.
Not every annotation works with operation_ for_version. For instance, the rename_ parameters_ as annotation directly modifies an object in the bleed-through stack, instead of putting new values in there. This means that using rename_ parameters_ as will modify every version of the annotation at once. In subsequent branches I'll be changing the BleedThroughDict data structure to work with this kind of annotation.