Merge lp:~wgrant/lazr.restful/cross-module-contribution into lp:lazr.restful

Proposed by William Grant
Status: Merged
Merged at revision: 205
Proposed branch: lp:~wgrant/lazr.restful/cross-module-contribution
Merge into: lp:lazr.restful
Diff against target: 295 lines (+81/-78)
4 files modified
src/lazr/restful/NEWS.txt (+8/-0)
src/lazr/restful/metazcml.py (+67/-65)
src/lazr/restful/testing/helpers.py (+0/-5)
src/lazr/restful/tests/test_declarations.py (+6/-8)
To merge this branch: bzr merge lp:~wgrant/lazr.restful/cross-module-contribution
Reviewer Review Type Date Requested Status
Celso Providelo (community) Approve
Review via email: mp+211682@code.launchpad.net

Commit message

Rework contributes_to handling to not require that an interface and its extensions all live in one webservice:register module.

Description of the change

The implementation of extension interfaces with contributes_to explodes with zope.configuration conflicts when an interface and its contributors span multiple webservice:register directives. find_interfaces_and_contributors is run once per webservice:register, and does all of the aggregation internally, not considering that there might be other modules involved:

zope.configuration.config.ConfigurationConflictError: Conflicting configuration actions
  For: ('webservice entry interface', <InterfaceClass lp.registry.interfaces.product.IProduct>)
    File "/home/wgrant/src/launchpad/branches/sandbox/lib/lp/blueprints/configure.zcml", line 248.2-248.70
        <webservice:register module="lp.blueprints.interfaces.webservice" />
    File "/home/wgrant/src/launchpad/branches/sandbox/lib/lp/registry/configure.zcml", line 2078.4-2078.70
          <webservice:register module="lp.registry.interfaces.webservice" />
  For: ('webservice versioned operations', <InterfaceClass lp.registry.interfaces.product.IProduct>)
    File "/home/wgrant/src/launchpad/branches/sandbox/lib/lp/blueprints/configure.zcml", line 248.2-248.70
        <webservice:register module="lp.blueprints.interfaces.webservice" />
    File "/home/wgrant/src/launchpad/branches/sandbox/lib/lp/registry/configure.zcml", line 2078.4-2078.70
          <webservice:register module="lp.registry.interfaces.webservice" />

This is a fatal flaw for the situation that contributes_to was meant to solve: it's not possible to have entry extensions pluggable by optional bits of ZCML.

This branch fixes extension interfaces to be calculated globally. I use a global dict in metazcml.py, cleared in zope.testing.cleanup like the other global Zopey state (see zope.component, zope.security, storm for examples). I had to use a custom configuration action order to ensure that all contributors are registered before we start generating entry and operation adapters.

To post a comment you must log in.
Revision history for this message
Celso Providelo (cprov) wrote :

Thanks for landing this change, it will help a lot the tasks we have in mind for LP and hopefully other large products using lazr.restful.

review: Approve
210. By William Grant

Update NEWS.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/lazr/restful/NEWS.txt'
2--- src/lazr/restful/NEWS.txt 2012-12-06 18:52:21 +0000
3+++ src/lazr/restful/NEWS.txt 2014-03-20 01:50:54 +0000
4@@ -2,12 +2,20 @@
5 NEWS for lazr.restful
6 =====================
7
8+0.9.11 (UNRELEASED)
9+===================
10+
11+Fixed bug 1294543: contributes_to can now reference interfaces in other
12+modules and webservice:register directives.
13+
14 0.19.10 (2012-12-06)
15+====================
16
17 Fixed bug 809863: WebServicePublicationMixin.getResouce() converts
18 ComponentLookupErrors to NotFound.
19
20 0.19.9 (2012-10-23)
21+===================
22
23 Fixed bug 924291: The FixedVocabularyFieldMarshaller will now correctly return
24 the entire vocabulary if the value passed in is None.
25
26=== modified file 'src/lazr/restful/metazcml.py'
27--- src/lazr/restful/metazcml.py 2011-03-24 21:29:49 +0000
28+++ src/lazr/restful/metazcml.py 2014-03-20 01:50:54 +0000
29@@ -6,6 +6,7 @@
30 __all__ = []
31
32
33+import collections
34 import inspect
35 import itertools
36
37@@ -54,6 +55,23 @@
38 REGISTERED_ENTRIES = []
39 REGISTERED_OPERATIONS = []
40
41+# Keep a global mapping of each interface's contributors, registered by
42+# contribute_to_interface and inspected by the entry generators.
43+REGISTERED_CONTRIBUTORS = collections.defaultdict(list)
44+
45+
46+def reset_globals():
47+ del REGISTERED_ENTRIES[:]
48+ del REGISTERED_OPERATIONS[:]
49+ REGISTERED_CONTRIBUTORS.clear()
50+
51+try:
52+ from zope.testing.cleanup import addCleanUp
53+except ImportError:
54+ pass
55+else:
56+ addCleanUp(reset_globals)
57+
58
59 class IRegisterDirective(Interface):
60 """Directive to hook up webservice based on the declarations in a module.
61@@ -62,8 +80,30 @@
62 title=u'Module which will be inspected for webservice declarations')
63
64
65-def generate_and_register_entry_adapters(interface, info, contributors,
66- repository=None):
67+def contribute_to_interface(interface, info):
68+ tag = interface.getTaggedValue(LAZR_WEBSERVICE_EXPORTED)
69+ for iface in tag['contributes_to']:
70+ if iface.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED) is None:
71+ raise AttemptToContributeToNonExportedInterface(
72+ "Interface %s contributes to %s, which is not "
73+ "exported." % (interface.__name__, iface.__name__))
74+
75+ # Check that no names conflict with the extended interface or
76+ # its contributors.
77+ contributors = REGISTERED_CONTRIBUTORS[iface]
78+ names = {}
79+ for iface in itertools.chain([iface, interface], contributors):
80+ for name, f in iface.namesAndDescriptions(all=True):
81+ if f.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED) is not None:
82+ L = names.setdefault(name, [])
83+ L.append(iface)
84+ for name, interfaces in names.items():
85+ if len(interfaces) > 1:
86+ raise ConflictInContributingInterfaces(name, interfaces)
87+ contributors.append(interface)
88+
89+
90+def generate_and_register_entry_adapters(interface, info, repository=None):
91 """Generate an entry adapter for every version of the web service.
92
93 This code generates an IEntry subinterface for every version, each
94@@ -75,6 +115,7 @@
95 # Get the list of versions.
96 config = getUtility(IWebServiceConfiguration)
97 versions = list(config.active_versions)
98+ contributors = REGISTERED_CONTRIBUTORS[interface]
99
100 # Generate an interface and an adapter for every version.
101 web_interfaces = generate_entry_interfaces(
102@@ -208,57 +249,6 @@
103 in inspect.getmembers(module, _is_exported_interface))
104
105
106-def find_interfaces_and_contributors(module):
107- """Find the interfaces and its contributors marked for export.
108-
109- Return a dictionary with interfaces as keys and their contributors as
110- values.
111- """
112- interfaces_with_contributors = {}
113- for interface in find_exported_interfaces(module):
114- if issubclass(interface, Exception):
115- # Exceptions can't have interfaces, so just store it in
116- # interfaces_with_contributors and move on.
117- interfaces_with_contributors.setdefault(interface, [])
118- continue
119-
120- tag = interface.getTaggedValue(LAZR_WEBSERVICE_EXPORTED)
121- if tag.get('contributes_to'):
122- # Append this interface (which is a contributing interface) to the
123- # list of contributors of every interface it contributes to.
124- for iface in tag['contributes_to']:
125- if iface.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED) is None:
126- raise AttemptToContributeToNonExportedInterface(
127- "Interface %s contributes to %s, which is not "
128- "exported." % (interface.__name__, iface.__name__))
129- raise AssertionError('foo')
130- contributors = interfaces_with_contributors.setdefault(
131- iface, [])
132- contributors.append(interface)
133- else:
134- # This is a regular interface, but one of its contributing
135- # interfaces may have been processed previously and in that case a
136- # key for it would already exist in interfaces_with_contributors;
137- # that's why we use setdefault.
138- interfaces_with_contributors.setdefault(interface, [])
139-
140- # For every exported interface, check that none of its names are exported
141- # in more than one contributing interface.
142- for interface, contributors in interfaces_with_contributors.items():
143- if len(contributors) == 0:
144- continue
145- names = {}
146- for iface in itertools.chain([interface], contributors):
147- for name, f in iface.namesAndDescriptions(all=True):
148- if f.queryTaggedValue(LAZR_WEBSERVICE_EXPORTED) is not None:
149- L = names.setdefault(name, [])
150- L.append(iface)
151- for name, interfaces in names.items():
152- if len(interfaces) > 1:
153- raise ConflictInContributingInterfaces(name, interfaces)
154- return interfaces_with_contributors
155-
156-
157 class ConflictInContributingInterfaces(Exception):
158 """More than one interface tried to contribute a given attribute/method to
159 another interface.
160@@ -287,21 +277,32 @@
161 if not inspect.ismodule(module):
162 raise TypeError("module attribute must be a module: %s, %s" %
163 module, type(module))
164- interfaces_with_contributors = find_interfaces_and_contributors(module)
165
166- for interface, contributors in interfaces_with_contributors.items():
167+ for interface in find_exported_interfaces(module):
168 if issubclass(interface, Exception):
169 register_exception_view(context, interface)
170 continue
171
172 tag = interface.getTaggedValue(LAZR_WEBSERVICE_EXPORTED)
173 if tag['type'] == ENTRY_TYPE:
174- context.action(
175- discriminator=('webservice entry interface', interface),
176- callable=generate_and_register_entry_adapters,
177- args=(interface, context.info, contributors,
178- REGISTERED_ENTRIES),
179- )
180+ if tag.get('contributes_to'):
181+ # This isn't a standalone entry, so just register its
182+ # contributions in the global map. This has to be done
183+ # before entry and operation adapter generation, so we
184+ # customise the order.
185+ context.action(
186+ discriminator=('webservice entry contributor', interface),
187+ callable=contribute_to_interface,
188+ args=(interface, context.info),
189+ order=1,
190+ )
191+ else:
192+ context.action(
193+ discriminator=('webservice entry interface', interface),
194+ callable=generate_and_register_entry_adapters,
195+ args=(interface, context.info, REGISTERED_ENTRIES),
196+ order=2,
197+ )
198 elif tag['type'] == COLLECTION_TYPE:
199 for version in tag['collection_default_content'].keys():
200 factory = generate_collection_adapter(interface, version)
201@@ -318,8 +319,9 @@
202 raise AssertionError('Unknown export type: %s' % tag['type'])
203 context.action(
204 discriminator=('webservice versioned operations', interface),
205- args=(context, interface, contributors, REGISTERED_OPERATIONS),
206- callable=generate_and_register_webservice_operations)
207+ args=(context, interface, REGISTERED_OPERATIONS),
208+ callable=generate_and_register_webservice_operations,
209+ order=2)
210
211 @adapter(IProcessStarting)
212 def webservice_sanity_checks(registration):
213@@ -448,7 +450,7 @@
214
215
216 def generate_and_register_webservice_operations(
217- context, interface, contributors, repository=None):
218+ context, interface, repository=None):
219 """Create and register adapters for all exported methods.
220
221 Different versions of the web service may publish the same
222@@ -470,7 +472,7 @@
223 block_mutator_operations_as_of_version = None
224
225 methods = interface.namesAndDescriptions(True)
226- for iface in contributors:
227+ for iface in REGISTERED_CONTRIBUTORS[interface]:
228 methods.extend(iface.namesAndDescriptions(True))
229
230 for name, method in methods:
231
232=== modified file 'src/lazr/restful/testing/helpers.py'
233--- src/lazr/restful/testing/helpers.py 2011-03-24 21:18:07 +0000
234+++ src/lazr/restful/testing/helpers.py 2014-03-20 01:50:54 +0000
235@@ -26,11 +26,6 @@
236
237 def register_test_module(name, *contents):
238 new_module = create_test_module(name, *contents)
239- # Clear out any registrations from other imports that weren't
240- # sanity-checked.
241- metazcml.REGISTERED_OPERATIONS = []
242- metazcml.REGISTERED_ENTRIES = []
243-
244 try:
245 xmlconfig.string("""
246 <configure
247
248=== modified file 'src/lazr/restful/tests/test_declarations.py'
249--- src/lazr/restful/tests/test_declarations.py 2011-07-27 13:27:33 +0000
250+++ src/lazr/restful/tests/test_declarations.py 2014-03-20 01:50:54 +0000
251@@ -58,7 +58,7 @@
252 from lazr.restful.metazcml import (
253 AttemptToContributeToNonExportedInterface,
254 ConflictInContributingInterfaces,
255- find_interfaces_and_contributors,
256+ contribute_to_interface,
257 generate_and_register_webservice_operations,
258 )
259 from lazr.restful._resource import (
260@@ -277,11 +277,10 @@
261 def test_duplicate_contributed_attributes(self):
262 # We do not allow a given attribute to be contributed to a given
263 # interface by more than one contributing interface.
264- testmod = create_test_module(
265- 'testmod', IBranch, IProduct, IHasBugs, IHasBugs2)
266+ contribute_to_interface(IHasBugs, {})
267 self.assertRaises(
268 ConflictInContributingInterfaces,
269- find_interfaces_and_contributors, testmod)
270+ contribute_to_interface, IHasBugs2, {})
271
272 def test_contributing_interface_not_exported(self):
273 # Contributing interfaces are not exported by themselves -- they only
274@@ -301,18 +300,17 @@
275 class IContributor(Interface):
276 export_as_webservice_entry(contributes_to=[INotExported])
277 title = exported(TextLine(title=u'The project title'))
278- testmod = create_test_module('testmod', IContributor, INotExported)
279 self.assertRaises(
280 AttemptToContributeToNonExportedInterface,
281- find_interfaces_and_contributors, testmod)
282+ contribute_to_interface, IContributor, {})
283
284 def test_duplicate_contributed_methods(self):
285 # We do not allow a given method to be contributed to a given
286 # interface by more than one contributing interface.
287- testmod = create_test_module('testmod', IProduct, IHasBugs, IHasBugs3)
288+ contribute_to_interface(IHasBugs, {})
289 self.assertRaises(
290 ConflictInContributingInterfaces,
291- find_interfaces_and_contributors, testmod)
292+ contribute_to_interface, IHasBugs3, {})
293
294 def test_ConflictInContributingInterfaces(self):
295 # The ConflictInContributingInterfaces exception states what are the

Subscribers

People subscribed via source and target branches