Merge lp:~benji/lazr.restful/deterministic-wadl-from-trunk into lp:lazr.restful

Proposed by Benji York
Status: Merged
Merged at revision: 142
Proposed branch: lp:~benji/lazr.restful/deterministic-wadl-from-trunk
Merge into: lp:lazr.restful
Diff against target: 268 lines (+97/-15)
6 files modified
src/lazr/restful/_resource.py (+7/-8)
src/lazr/restful/tales.py (+4/-3)
src/lazr/restful/tests/test_utils.py (+26/-1)
src/lazr/restful/tests/test_webservice.py (+52/-2)
src/lazr/restful/utils.py (+7/-0)
versions.cfg (+1/-1)
To merge this branch: bzr merge lp:~benji/lazr.restful/deterministic-wadl-from-trunk
Reviewer Review Type Date Requested Status
Leonard Richardson (community) Approve
Review via email: mp+33577@code.launchpad.net

Commit message

Make the generated WADL consistent for a given input so it doesn't vary from one call to the next.

Description of the change

Make the generated WADL consistent for a given input so it doesn't vary from one call to the next.

To post a comment you must log in.
141. By Benji York

remove unneeded version pin

Revision history for this message
Leonard Richardson (leonardr) wrote :

Needs some minor changes:

1. Move sorted_named_things() to utils.py and its test to doc/utils.txt
2. Get rid of the 'distribute' line.
3. You don't need to use exec(). You can create your own InterfaceClass with a custom __dict__. Look at declarations.py for an example.

        entry_interface = InterfaceClass(
            class_name, bases=(IEntry, ), attrs=attrs,
            __doc__=interface.__doc__, __module__=interface.__module__)

review: Needs Fixing
142. By Benji York

checkpoint review-inspired refactorings; it works but part of it is nasty

143. By Benji York

remove unneeded line

Revision history for this message
Benji York (benji) wrote :

On Tue, Aug 24, 2010 at 4:13 PM, Leonard Richardson
<email address hidden> wrote:
> Review: Needs Fixing
> Needs some minor changes:
>
> 1. Move sorted_named_things() to utils.py and its test to doc/utils.txt
> 2. Get rid of the 'distribute' line.

The above two are done.

> 3. You don't need to use exec(). You can create your own InterfaceClass with a custom __dict__. Look at declarations.py for an example.
>
>        entry_interface = InterfaceClass(
>            class_name, bases=(IEntry, ), attrs=attrs,
>            __doc__=interface.__doc__, __module__=interface.__module__)

To get the above to work I had to emulate the effects of
generate_entry_interfaces (by copying a big chunk of code out of it)
because it is only usable as an old-style class decorator.

This cure seems worse than the disease. We can 1) refactor the bits
that I need into a callable and call it or 2) put the exec back.

I'd rather go with number 2 because the test code is easier to
understand (despite the exec) because it is doing the same things a
"normal" piece of code would do to reach the same ends (i.e., using
generate_entry_interfaces as a class decorator), instead of calling a
function who's only call site is in a test.

Strong opinions one way or the other?
--
Benji York

Revision history for this message
Leonard Richardson (leonardr) wrote :

> To get the above to work I had to emulate the effects of
> generate_entry_interfaces (by copying a big chunk of code out of it)
> because it is only usable as an old-style class decorator.

I see. Although it would be nice to be able to create dynamic interfaces, but since only one test needs to do it right now, I'm fine with the exec(). Go ahead and change it back, and merge the branch.

Revision history for this message
Leonard Richardson (leonardr) :
review: Approve
144. By Benji York

restore slightly icky code that is better than the really icky code

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/lazr/restful/_resource.py'
2--- src/lazr/restful/_resource.py 2010-08-18 16:41:58 +0000
3+++ src/lazr/restful/_resource.py 2010-08-25 13:24:44 +0000
4@@ -28,16 +28,13 @@
5 ]
6
7
8+from datetime import datetime, date
9+from email.Utils import formatdate
10 import cgi
11 import copy
12-from cStringIO import StringIO
13-from datetime import datetime, date
14-from email.Utils import formatdate
15-from gzip import GzipFile
16 import os
17 import simplejson
18 import time
19-import zlib
20
21 # Import SHA in a way compatible with both Python 2.4 and Python 2.6.
22 try:
23@@ -90,7 +87,8 @@
24 IUnmarshallingDoesntNeedValue, IWebServiceClientRequest,
25 IWebServiceConfiguration, IWebServiceLayer, IWebServiceVersion,
26 LAZR_WEBSERVICE_NAME)
27-from lazr.restful.utils import get_current_web_service_request
28+from lazr.restful.utils import (
29+ get_current_web_service_request, sorted_named_things)
30
31
32 # The path to the WADL XML Schema definition.
33@@ -1900,11 +1898,12 @@
34 # We omit IScopedCollection because those are handled
35 # by the entry classes.
36 collection_classes.append(registration.factory)
37+
38 namespace = self.WADL_TEMPLATE.pt_getContext()
39 namespace['service'] = self
40 namespace['request'] = self.request
41- namespace['entries'] = entry_classes
42- namespace['collections'] = collection_classes
43+ namespace['entries'] = sorted_named_things(entry_classes)
44+ namespace['collections'] = sorted_named_things(collection_classes)
45 return self.WADL_TEMPLATE.pt_render(namespace)
46
47 def toDataForJSON(self):
48
49=== modified file 'src/lazr/restful/tales.py'
50--- src/lazr/restful/tales.py 2010-08-09 20:05:08 +0000
51+++ src/lazr/restful/tales.py 2010-08-25 13:24:44 +0000
52@@ -6,6 +6,7 @@
53
54 all = ['entry_adapter_for_schema']
55
56+import operator
57 import simplejson
58 import textwrap
59 import urllib
60@@ -17,7 +18,7 @@
61 adapts, getGlobalSiteManager, getUtility, queryMultiAdapter)
62 from zope.interface import implements
63 from zope.interface.interfaces import IInterface
64-from zope.schema import getFieldsInOrder
65+from zope.schema import getFields
66 from zope.schema.interfaces import IBytes, IChoice, IDate, IDatetime, IObject
67 from zope.security.proxy import removeSecurityProxy
68 from zope.publisher.interfaces.browser import IBrowserRequest
69@@ -255,7 +256,7 @@
70 resource_dicts.append({'name' : link_name,
71 'path' : "$['%s']" % link_name,
72 'resource' : resource})
73- return resource_dicts
74+ return sorted(resource_dicts, key=operator.itemgetter('name'))
75
76
77 class WadlResourceAdapterAPI(RESTUtilityBase):
78@@ -387,7 +388,7 @@
79 def all_fields(self):
80 "Return all schema fields for the object."
81 return [field for name, field in
82- getFieldsInOrder(self.adapter.schema)]
83+ sorted(getFields(self.adapter.schema).items())]
84
85 @property
86 def all_writable_fields(self):
87
88=== modified file 'src/lazr/restful/tests/test_utils.py'
89--- src/lazr/restful/tests/test_utils.py 2010-08-05 19:06:49 +0000
90+++ src/lazr/restful/tests/test_utils.py 2010-08-25 13:24:44 +0000
91@@ -4,6 +4,7 @@
92
93 __metaclass__ = type
94
95+import random
96 import unittest
97
98 from zope.publisher.browser import TestRequest
99@@ -11,7 +12,7 @@
100 endInteraction, newInteraction, queryInteraction)
101
102 from lazr.restful.utils import (get_current_browser_request,
103- is_total_size_link_active)
104+ is_total_size_link_active, sorted_named_things)
105
106
107 class TestUtils(unittest.TestCase):
108@@ -50,6 +51,30 @@
109 self.assertEqual(is_total_size_link_active('2.0', FakeConfig), True)
110 self.assertEqual(is_total_size_link_active('3.0', FakeConfig), True)
111
112+ def test_name_sorter(self):
113+ # The WADL generation often sorts classes or functions by name; the
114+ # sorted_named_things helper... helps.
115+
116+ # First we need some named things to sort.
117+ class Thing:
118+ pass
119+
120+ def make_named_thing():
121+ thing = Thing()
122+ thing.__name__ = random.choice('abcdefghijk')
123+ return thing
124+
125+ # The function sorts on the object's __name__ attribute, which
126+ # functions and classes have, see:
127+ assert hasattr(Thing, '__name__')
128+ assert hasattr(make_named_thing, '__name__')
129+
130+ # Now we can make a bunch of things with randomly ordered names and
131+ # show that sorting them does order them by name.
132+ things = sorted_named_things(make_named_thing() for i in range(10))
133+ names = [thing.__name__ for thing in things]
134+ self.assertEqual(names, sorted(names))
135+
136 # For the sake of convenience, test_get_current_web_service_request()
137 # and tag_request_with_version_name() are tested in test_webservice.py.
138
139
140=== modified file 'src/lazr/restful/tests/test_webservice.py'
141--- src/lazr/restful/tests/test_webservice.py 2010-08-10 18:36:09 +0000
142+++ src/lazr/restful/tests/test_webservice.py 2010-08-25 13:24:44 +0000
143@@ -6,10 +6,14 @@
144
145 from cStringIO import StringIO
146 from operator import attrgetter
147+from textwrap import dedent
148+import random
149+import re
150 import unittest
151
152 from zope.component import getGlobalSiteManager, getUtility
153 from zope.interface import implements, Interface
154+from zope.interface.interface import InterfaceClass
155 from zope.publisher.browser import TestRequest
156 from zope.schema import Date, Datetime, TextLine
157 from zope.security.management import newInteraction, queryInteraction
158@@ -22,14 +26,14 @@
159 IWebServiceConfiguration, IWebServiceClientRequest, IWebServiceVersion)
160 from lazr.restful import EntryResource, ResourceGETOperation
161 from lazr.restful.declarations import (
162- exported, export_as_webservice_entry, LAZR_WEBSERVICE_NAME)
163+ exported, export_as_webservice_entry, LAZR_WEBSERVICE_NAME, annotate_exported_methods)
164 from lazr.restful.testing.webservice import (
165 create_web_service_request, IGenericCollection, IGenericEntry,
166 WebServiceTestCase)
167 from lazr.restful.testing.tales import test_tales
168 from lazr.restful.utils import (
169 get_current_browser_request, get_current_web_service_request,
170- tag_request_with_version_name)
171+ tag_request_with_version_name, sorted_named_things)
172 from lazr.restful._resource import CollectionResource, BatchingResourceMixin
173
174
175@@ -277,6 +281,52 @@
176 self.assertEquals(str(e), expected_error_message)
177
178
179+def make_entry(name):
180+ """Make an entity with some attibutes to expose as a web service."""
181+ code = """
182+ class %(name)s(Interface):
183+ export_as_webservice_entry(singular_name='%(name)s')
184+ """ % locals()
185+
186+ for letter in 'rstuvwxyz':
187+ code += """
188+ %(letter)s_field = exported(TextLine(title=u'Field %(letter)s'))
189+ """ % locals()
190+
191+ exec dedent(code)
192+ return locals()[name]
193+
194+
195+class TestWadlDeterminism(WebServiceTestCase):
196+ """We want the WADL generation to be consistent for a given input."""
197+
198+ def __init__(self, *args, **kwargs):
199+ # make some -- randomly ordered -- objects to use to build the WADL
200+ self.testmodule_objects = [make_entry(name) for name in 'abcdefghijk']
201+ random.shuffle(self.testmodule_objects)
202+ super(TestWadlDeterminism, self).__init__(*args, **kwargs)
203+
204+ @property
205+ def wadl(self):
206+ resource = getUtility(IServiceRootResource)
207+ request = create_web_service_request('/2.0')
208+ request.traverse(resource)
209+ return resource.toWADL()
210+
211+ def test_entity_order(self):
212+ # The entities should be listed in alphabetical order by class.
213+ self.assertEqual(
214+ re.findall(r'<wadl:resource_type id="(.)">', self.wadl),
215+ ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k'])
216+
217+ def test_attribute_order(self):
218+ # The individual entity attributes should be listed in alphabetical
219+ # order by class.
220+ self.assertEqual(
221+ re.findall(r'<wadl:param [^>]* name="(.)_field">', self.wadl)[:9],
222+ ['r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'])
223+
224+
225 class DuplicateSingularNameTestCase(DuplicateNameTestCase):
226 """Test AssertionError when resource types share a singular name."""
227
228
229=== modified file 'src/lazr/restful/utils.py'
230--- src/lazr/restful/utils.py 2010-08-05 14:08:20 +0000
231+++ src/lazr/restful/utils.py 2010-08-25 13:24:44 +0000
232@@ -20,6 +20,7 @@
233
234 import cgi
235 import copy
236+import operator
237 import re
238 import string
239 import subprocess
240@@ -282,3 +283,9 @@
241 )
242 (output, nothing) = p.communicate(input)
243 return output
244+
245+
246+def sorted_named_things(things):
247+ """Return a list of things (functions and/or classes) sorted by name."""
248+ name = operator.attrgetter('__name__')
249+ return sorted(things, key=name)
250
251=== modified file 'versions.cfg'
252--- versions.cfg 2010-08-18 15:33:46 +0000
253+++ versions.cfg 2010-08-25 13:24:44 +0000
254@@ -6,13 +6,13 @@
255 [versions]
256 # Alphabetical, case-SENSITIVE, blank line after this comment
257
258-distribute = 0.6.14
259 Jinja2 = 2.5
260 Pygments = 1.3.1
261 RestrictedPython = 3.5.1
262 Sphinx = 1.0.1
263 ZConfig = 2.7.1
264 ZODB3 = 3.9.2
265+distribute = 0.6.14
266 docutils = 0.5
267 epydoc = 3.0.1
268 grokcore.component = 1.6

Subscribers

People subscribed via source and target branches