Merge lp:~leonardr/lazr.restful/multiversion-destructors into lp:lazr.restful

Proposed by Leonard Richardson
Status: Merged
Merged at revision: not available
Proposed branch: lp:~leonardr/lazr.restful/multiversion-destructors
Merge into: lp:lazr.restful
Diff against target: 227 lines (+94/-29)
2 files modified
src/lazr/restful/declarations.py (+37/-23)
src/lazr/restful/docs/webservice-declarations.txt (+57/-6)
To merge this branch: bzr merge lp:~leonardr/lazr.restful/multiversion-destructors
Reviewer Review Type Date Requested Status
Paul Hummer (community) code Approve
Review via email: mp+18643@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote :

This branch adds multi-version error checking for destructor methods. A destructor method cannot have any arguments that are not fixed to specific values. The previous code was not version-aware, so it was only checking for the most recent version of the web service. In this branch I check every version in which the destructor method is published.

I also defined a tiny helper method, _version_name, which takes care of this logic which I was putting all over the place when printing out error messages:

    if version is None:
        version = "(earliest version)"

Revision history for this message
Paul Hummer (rockstar) wrote :

Based on previous knowledge from reviewing the dependent branch, this branch is a bit easier to grok. Once again, your doctests make this patch easier to understand.

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-02-04 21:50:06 +0000
3+++ src/lazr/restful/declarations.py 2010-02-04 21:53:10 +0000
4@@ -285,11 +285,10 @@
5 'collection_default_content', {})
6
7 if version in default_content_methods:
8- if version is None:
9- version = "(earliest version)"
10 raise TypeError(
11 "Only one method can be marked with "
12- "@collection_default_content for version '%s'." % version)
13+ "@collection_default_content for version '%s'." % (
14+ _version_name(version)))
15 self.version = version
16 self.params = params
17
18@@ -424,11 +423,6 @@
19 # Make sure that each version of the web service defines
20 # a self-consistent view of this method.
21 for version, annotations in annotation_stack.stack:
22- if version is None:
23- # Create a human-readable name for the earliest version
24- # without trying to look it up.
25- version = "(earliest version)"
26-
27 if annotations['type'] == REMOVED_OPERATION_TYPE:
28 # The method is published in other versions of the web
29 # service, but not in this one. Don't try to validate this
30@@ -456,15 +450,15 @@
31 raise TypeError(
32 'method "%s" doesn\'t have the following exported '
33 'parameters in version "%s": %s.' % (
34- method.__name__, version,
35+ method.__name__, _version_name(version),
36 ", ".join(sorted(undefined_params))))
37 missing_params = set(
38 info['required']).difference(exported_params)
39 if missing_params:
40 raise TypeError(
41- 'method "%s" is missing exported parameter definitions '
42- 'in version "%s": %s' % (
43- method.__name__, version,
44+ 'method "%s" is missing definitions for parameter(s) '
45+ 'exported in version "%s": %s' % (
46+ method.__name__, _version_name(version),
47 ", ".join(sorted(missing_params))))
48
49 _update_default_and_required_params(annotations['params'], info)
50@@ -784,21 +778,29 @@
51 """
52 type = "destructor"
53
54- def annotate_method(self, method, annotations):
55+ def annotate_method(self, method, annotation_stack):
56 """See `_method_annotator`.
57
58 Store information about the mutator method with the method.
59+
60+ Every version must have a self-consistent set of annotations.
61 """
62+ super(export_destructor_operation, self).annotate_method(
63+ method, annotation_stack)
64 # The mutator method must take no arguments, not counting
65 # arguments with values fixed by call_with().
66 signature = fromFunction(method).getSignatureInfo()
67- free_params = _free_parameters(method, annotations)
68- if len(free_params) != 0:
69- raise TypeError("A destructor method must take no "
70- "non-fixed arguments; %s takes %d." %
71- (method.__name__, len(free_params)))
72- super(export_destructor_operation, self).annotate_method(
73- method, annotations)
74+ for version, annotations in annotation_stack.stack:
75+ if annotations['type'] == REMOVED_OPERATION_TYPE:
76+ continue
77+ free_params = _free_parameters(method, annotations)
78+ if len(free_params) != 0:
79+ raise TypeError(
80+ "A destructor method must take no non-fixed arguments. "
81+ 'In version %s, the "%s" method takes %d: "%s".' % (
82+ _version_name(version), method.__name__,
83+ len(free_params), '", "'.join(free_params))
84+ )
85
86
87 def _check_tagged_interface(interface, type):
88@@ -845,12 +847,11 @@
89 if annotations.get('type') == export_destructor_operation.type:
90 destructor = destructor_for_version.get(version)
91 if destructor is not None:
92- if version is None:
93- version = "(earliest)"
94 raise TypeError(
95 'An entry can only have one destructor method for '
96 'version %s; %s and %s make two.' % (
97- version, method.__name__, destructor.__name__))
98+ _version_name(version), method.__name__,
99+ destructor.__name__))
100 destructor_for_version[version] = method
101
102 # Next, we'll normalize each published field. A normalized field
103@@ -1267,6 +1268,19 @@
104 versioned_dict.stack = new_stack
105 return field
106
107+
108+def _version_name(version):
109+ """Return a human-readable version name.
110+
111+ If `version` is None (indicating the as-yet-unknown earliest
112+ version), returns "(earliest version)". Otherwise returns the version
113+ name.
114+ """
115+ if version is None:
116+ return "(earliest version)"
117+ return version
118+
119+
120 def _versioned_class_name(base_name, version):
121 """Create a class name incorporating the given version string."""
122 if version is None:
123
124=== modified file 'src/lazr/restful/docs/webservice-declarations.txt'
125--- src/lazr/restful/docs/webservice-declarations.txt 2010-02-03 20:59:22 +0000
126+++ src/lazr/restful/docs/webservice-declarations.txt 2010-02-04 21:53:10 +0000
127@@ -534,8 +534,8 @@
128 ... def a_method(param1, param2, param3, param4): pass
129 Traceback (most recent call last):
130 ...
131- TypeError: method "a_method" is missing exported parameter definitions
132- in version "(earliest version)": param3, param4
133+ TypeError: method "a_method" is missing definitions for parameter(s)
134+ exported in version "(earliest version)": param3, param4
135
136 Defining a parameter not available on the method also results in an
137 error:
138@@ -1281,12 +1281,14 @@
139 ... text = exported(TextLine(readonly=True))
140 ...
141 ... @export_destructor_operation()
142+ ... @operation_parameters(argument=TextLine())
143 ... def destroy(argument):
144 ... pass
145 Traceback (most recent call last):
146 ...
147- TypeError: A destructor method must take no non-fixed arguments;
148- destroy takes 1.
149+ TypeError: A destructor method must take no non-fixed arguments.
150+ In version (earliest version), the "destroy" method takes 1:
151+ "argument".
152
153 >>> class IHasText(Interface):
154 ... export_as_webservice_entry()
155@@ -1298,7 +1300,6 @@
156 ... pass
157 >>> ignored = generate_entry_interfaces(IHasText, 'beta')
158
159-
160 An entry cannot have more than one destructor.
161
162 >>> from lazr.restful.declarations import export_destructor_operation
163@@ -1317,7 +1318,7 @@
164 Traceback (most recent call last):
165 ...
166 TypeError: An entry can only have one destructor method for
167- version (earliest); destroy and destroy2 make two.
168+ version (earliest version); destroy and destroy2 make two.
169
170
171 Mutators
172@@ -2247,6 +2248,56 @@
173 [XXX leonardr mutator_for modifies the field, not the method, so it
174 won't work until I add multiversion support for fields.]
175
176+Destructor operations
177+*********************
178+
179+A destructor can be published in different ways in different versions,
180+but the restrictions on destructor arguments are enforced separately
181+for each version.
182+
183+Here, the destructor fixes a value for the 'fixed2' argument in the
184+earliest version, but not in '1.0'. This is fine: the 1.0 value for
185+'fixed2' will be inherited from the previous version.
186+
187+ >>> class IGoodDestructorEntry(Interface):
188+ ... export_as_webservice_entry()
189+ ...
190+ ... @call_with(fixed1="value3")
191+ ... @operation_for_version('1.0')
192+ ... @export_destructor_operation()
193+ ... @call_with(fixed1="value1", fixed2="value")
194+ ... @operation_parameters(fixed1=TextLine(), fixed2=TextLine())
195+ ... def destructor(fixed1, fixed2):
196+ ... """Another destructor method."""
197+
198+ >>> ignore = generate_entry_interfaces(
199+ ... IGoodDestructorEntry, 'beta', '1.0')
200+
201+This is not fine. In this case, the destructor is removed in 1.0 and
202+added back in 2.0. The 2.0 version does not inherit any values from
203+its prior incarnation, so the fact that it does not fix any value for
204+'fixed2' is a problem.
205+
206+ >>> class IBadDestructorEntry(Interface):
207+ ... export_as_webservice_entry()
208+ ...
209+ ... @export_destructor_operation()
210+ ... @operation_parameters(fixed1=TextLine(), fixed2=TextLine())
211+ ... @call_with(fixed1="value3")
212+ ... @operation_for_version('2.0')
213+ ... @operation_removed_in_version('1.0')
214+ ... @export_destructor_operation()
215+ ... @call_with(fixed1="value1", fixed2="value")
216+ ... @operation_parameters(fixed1=TextLine(), fixed2=TextLine())
217+ ... def destructor(fixed1, fixed2):
218+ ... """Another destructor method."""
219+ Traceback (most recent call last):
220+ ...
221+ TypeError: A destructor method must take no non-fixed
222+ arguments. In version 2.0, the "destructor" method takes 1:
223+ "fixed2".
224+
225+
226 Security
227 ========
228

Subscribers

People subscribed via source and target branches