Merge lp:~jml/launchpad/more-services into lp:launchpad

Proposed by Jonathan Lange
Status: Merged
Merged at revision: not available
Proposed branch: lp:~jml/launchpad/more-services
Merge into: lp:launchpad
Diff against target: 800 lines (+208/-237)
15 files modified
lib/canonical/launchpad/scripts/__init__.py (+0/-3)
lib/canonical/launchpad/xmlrpc/faults.py (+1/-35)
lib/canonical/launchpad/xmlrpc/tests/test_authserver.py (+2/-8)
lib/canonical/launchpad/xmlrpc/tests/test_faults.py (+0/-65)
lib/lp/code/xmlrpc/tests/test_branch.py (+2/-1)
lib/lp/code/xmlrpc/tests/test_codehosting.py (+20/-34)
lib/lp/codehosting/sshserver/accesslog.py (+1/-15)
lib/lp/codehosting/sshserver/auth.py (+1/-1)
lib/lp/codehosting/vfs/branchfs.py (+3/-2)
lib/lp/codehosting/vfs/branchfsclient.py (+0/-18)
lib/lp/codehosting/vfs/tests/test_branchfsclient.py (+1/-54)
lib/lp/services/twistedsupport/tests/test_xmlrpc.py (+90/-0)
lib/lp/services/twistedsupport/xmlrpc.py (+27/-0)
lib/lp/services/utils.py (+20/-1)
lib/lp/services/xmlrpc.py (+40/-0)
To merge this branch: bzr merge lp:~jml/launchpad/more-services
Reviewer Review Type Date Requested Status
Abel Deuring (community) code Approve
Review via email: mp+23107@code.launchpad.net

Commit message

Move Twisted fault support and the base LaunchpadFault to lp.services. Move 'synchronize' helper to lp.services.

Description of the change

This branch extracts some drive-by-cleanups from my uber-branch lp:~jml/launchpad/ssh-key-auth.

Specifically:
  1. The higher-order function "synchronize" is moved to lp.services
  2. The "trap_fault" helper for XMLRPC Twisted code is moved to lp.services.twistedsupport
  3. The "LaunchpadFault" base class for all of our XMLRPC faults is moved to lp.services.xmlrpc
  4. A XXX comment from 2005 is deleted

I know it's a bit of a grab-bag of changes, but they are all conceptually quite simple, so I hope it's an easy enough branch to review.

To post a comment you must log in.
Revision history for this message
Abel Deuring (adeuring) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/scripts/__init__.py'
2--- lib/canonical/launchpad/scripts/__init__.py 2010-03-18 21:30:51 +0000
3+++ lib/canonical/launchpad/scripts/__init__.py 2010-04-10 13:58:26 +0000
4@@ -46,9 +46,6 @@
5 # We should probably split out all the stuff in this directory that
6 # doesn't rely on Zope and migrate it to canonical/scripts.
7
8-# XXX SteveAlexander 2005-04-11:
9-# This is a total mess. I need to work out what this all means.
10-
11 class NullItem:
12 def __init__(self, context, handler, info, *argdata):
13 newcontext = GroupingContextDecorator(context)
14
15=== modified file 'lib/canonical/launchpad/xmlrpc/faults.py'
16--- lib/canonical/launchpad/xmlrpc/faults.py 2010-03-14 20:15:34 +0000
17+++ lib/canonical/launchpad/xmlrpc/faults.py 2010-04-10 13:58:26 +0000
18@@ -15,7 +15,6 @@
19 'BranchNameInUse',
20 'BranchUniqueNameConflict',
21 'CannotHaveLinkedBranch',
22- 'check_fault',
23 'FileBugGotProductAndDistro',
24 'FileBugMissingProductOrDistribution',
25 'InvalidBranchIdentifier',
26@@ -42,41 +41,8 @@
27 ]
28
29
30-import xmlrpclib
31-
32-
33 from lp.registry.interfaces.projectgroup import IProjectGroup
34-
35-
36-def check_fault(fault, *fault_classes):
37- """Check if 'fault's faultCode matches any of 'fault_classes'.
38-
39- :param fault: An instance of `xmlrpclib.Fault`.
40- :param fault_classes: Any number of `LaunchpadFault` subclasses.
41- """
42- for cls in fault_classes:
43- if fault.faultCode == cls.error_code:
44- return True
45- return False
46-
47-
48-class LaunchpadFault(xmlrpclib.Fault):
49- """Base class for a Launchpad XMLRPC fault.
50-
51- Subclasses should define a unique error_code and a msg_template,
52- which will be interpolated with the given keyword arguments.
53- """
54-
55- error_code = None
56- msg_template = None
57-
58- def __init__(self, **kw):
59- assert self.error_code is not None, (
60- "Subclasses must define error_code.")
61- assert self.msg_template is not None, (
62- "Subclasses must define msg_template.")
63- msg = self.msg_template % kw
64- xmlrpclib.Fault.__init__(self, self.error_code, msg)
65+from lp.services.xmlrpc import LaunchpadFault
66
67
68 class NoSuchProduct(LaunchpadFault):
69
70=== modified file 'lib/canonical/launchpad/xmlrpc/tests/test_authserver.py'
71--- lib/canonical/launchpad/xmlrpc/tests/test_authserver.py 2009-06-25 05:30:52 +0000
72+++ lib/canonical/launchpad/xmlrpc/tests/test_authserver.py 2010-04-10 13:58:26 +0000
73@@ -16,6 +16,7 @@
74 from canonical.launchpad.xmlrpc.authserver import AuthServerAPIView
75 from canonical.testing.layers import DatabaseFunctionalLayer
76
77+
78 class GetUserAndSSHKeysTests(TestCaseWithFactory):
79 """Tests for the implementation of `IAuthServer.getUserAndSSHKeys`.
80 """
81@@ -28,17 +29,10 @@
82 self.authserver = AuthServerAPIView(
83 private_root.authserver, TestRequest())
84
85- def assertFaultEqual(self, expected_fault, observed_fault):
86- """Assert that `expected_fault` equals `observed_fault`."""
87- self.assertIsInstance(observed_fault, faults.LaunchpadFault)
88- self.assertEqual(expected_fault.faultCode, observed_fault.faultCode)
89- self.assertEqual(
90- expected_fault.faultString, observed_fault.faultString)
91-
92 def test_user_not_found(self):
93 # getUserAndSSHKeys returns the NoSuchPersonWithName fault if there is
94 # no Person of the given name.
95- self.assertFaultEqual(
96+ self.assertEqual(
97 faults.NoSuchPersonWithName('no-one'),
98 self.authserver.getUserAndSSHKeys('no-one'))
99
100
101=== removed file 'lib/canonical/launchpad/xmlrpc/tests/test_faults.py'
102--- lib/canonical/launchpad/xmlrpc/tests/test_faults.py 2009-06-25 05:30:52 +0000
103+++ lib/canonical/launchpad/xmlrpc/tests/test_faults.py 1970-01-01 00:00:00 +0000
104@@ -1,65 +0,0 @@
105-# Copyright 2009 Canonical Ltd. This software is licensed under the
106-# GNU Affero General Public License version 3 (see the file LICENSE).
107-
108-"""Tests for `canonical.launchpad.xmlrpc.faults`."""
109-
110-__metaclass__ = type
111-
112-import unittest
113-
114-from lp.testing import TestCase
115-from canonical.launchpad.xmlrpc import faults
116-
117-
118-class TestFaultOne(faults.LaunchpadFault):
119- """An arbitrary subclass of `LaunchpadFault`.
120-
121- This class and `TestFaultTwo` are a pair of distinct `LaunchpadFault`
122- subclasses to use in tests.
123- """
124-
125- error_code = 1001
126- msg_template = "Fault one."
127-
128-
129-class TestFaultTwo(faults.LaunchpadFault):
130- """Another arbitrary subclass of `LaunchpadFault`.
131-
132- This class and `TestFaultOne` are a pair of distinct `LaunchpadFault`
133- subclasses to use in tests.
134- """
135-
136- error_code = 1002
137- msg_template = "Fault two."
138-
139-
140-class TestTrapFault(TestCase):
141- """Tests for `check_fault`."""
142-
143- def test_wrong_fault(self):
144- # check_fault returns False if the passed fault does not have the code
145- # of one of the passed classes.
146- self.assertFalse(
147- faults.check_fault(TestFaultOne(), TestFaultTwo))
148-
149- def test_no_fault_classes(self):
150- # check_fault returns False if there are no passed classes.
151- self.assertFalse(
152- faults.check_fault(TestFaultOne()))
153-
154- def test_matches(self):
155- # check_fault returns True if the passed fault has the code of the
156- # passed class.
157- self.assertTrue(
158- faults.check_fault(TestFaultOne(), TestFaultOne))
159-
160- def test_matches_one_of_set(self):
161- # check_fault returns True if the passed fault has the code of one of
162- # the passed classes.
163- self.assertTrue(faults.check_fault(
164- TestFaultOne(), TestFaultOne, TestFaultTwo))
165-
166-
167-def test_suite():
168- return unittest.TestLoader().loadTestsFromName(__name__)
169-
170
171=== modified file 'lib/lp/code/xmlrpc/tests/test_branch.py'
172--- lib/lp/code/xmlrpc/tests/test_branch.py 2010-02-22 06:16:47 +0000
173+++ lib/lp/code/xmlrpc/tests/test_branch.py 2010-04-10 13:58:26 +0000
174@@ -21,6 +21,7 @@
175 from lp.testing import TestCaseWithFactory
176 from lazr.uri import URI
177 from lp.code.xmlrpc.branch import PublicCodehostingAPI
178+from lp.services.xmlrpc import LaunchpadFault
179 from canonical.launchpad.xmlrpc import faults
180 from canonical.testing import DatabaseFunctionalLayer
181
182@@ -62,7 +63,7 @@
183 """Assert that `lp_url_path` path expands to `unique_name`."""
184 results = self.api.resolve_lp_path(lp_url_path)
185 # This improves the error message if results happens to be a fault.
186- if isinstance(results, faults.LaunchpadFault):
187+ if isinstance(results, LaunchpadFault):
188 raise results
189 for url in results['urls']:
190 self.assertEqual('/' + unique_name, URI(url).path)
191
192=== modified file 'lib/lp/code/xmlrpc/tests/test_codehosting.py'
193--- lib/lp/code/xmlrpc/tests/test_codehosting.py 2010-02-24 19:01:25 +0000
194+++ lib/lp/code/xmlrpc/tests/test_codehosting.py 2010-04-10 13:58:26 +0000
195@@ -137,13 +137,6 @@
196 self.branch_lookup = frontend.getBranchLookup()
197 self.getLastActivity = frontend.getLastActivity
198
199- def assertFaultEqual(self, expected_fault, observed_fault):
200- """Assert that `expected_fault` equals `observed_fault`."""
201- self.assertIsInstance(observed_fault, faults.LaunchpadFault)
202- self.assertEqual(expected_fault.faultCode, observed_fault.faultCode)
203- self.assertEqual(
204- expected_fault.faultString, observed_fault.faultString)
205-
206 def assertMirrorFailed(self, branch, failure_message, num_failures=1):
207 """Assert that `branch` failed to mirror.
208
209@@ -204,7 +197,7 @@
210 # exist.
211 invalid_id = self.getUnusedBranchID()
212 fault = self.storage.startMirroring(invalid_id)
213- self.assertFaultEqual(faults.NoBranchWithID(invalid_id), fault)
214+ self.assertEqual(faults.NoBranchWithID(invalid_id), fault)
215
216 def test_mirrorFailed(self):
217 branch = self.factory.makeAnyBranch()
218@@ -220,7 +213,7 @@
219 branch_id = self.getUnusedBranchID()
220 failure_message = self.factory.getUniqueString()
221 fault = self.storage.mirrorFailed(branch_id, failure_message)
222- self.assertFaultEqual(faults.NoBranchWithID(branch_id), fault)
223+ self.assertEqual(faults.NoBranchWithID(branch_id), fault)
224
225 def test_mirrorComplete(self):
226 # mirrorComplete marks the branch as having been successfully
227@@ -240,7 +233,7 @@
228 branch_id = self.getUnusedBranchID()
229 fault = self.storage.mirrorComplete(
230 branch_id, self.factory.getUniqueString())
231- self.assertFaultEqual(faults.NoBranchWithID(branch_id), fault)
232+ self.assertEqual(faults.NoBranchWithID(branch_id), fault)
233
234 def test_mirrorComplete_resets_failure_count(self):
235 # mirrorComplete marks the branch as successfully mirrored and removes
236@@ -388,7 +381,7 @@
237 stacked_branch = self.factory.makeAnyBranch()
238 url = self.factory.getUniqueURL()
239 fault = self.storage.setStackedOn(stacked_branch.id, url)
240- self.assertFaultEqual(faults.NoSuchBranch(url), fault)
241+ self.assertEqual(faults.NoSuchBranch(url), fault)
242
243 def test_setStackedOnNoBranchWithID(self):
244 # If setStackedOn is called for a branch that doesn't exist, it will
245@@ -397,7 +390,7 @@
246 branch_type=BranchType.MIRRORED)
247 branch_id = self.getUnusedBranchID()
248 fault = self.storage.setStackedOn(branch_id, stacked_on_branch.url)
249- self.assertFaultEqual(faults.NoBranchWithID(branch_id), fault)
250+ self.assertEqual(faults.NoBranchWithID(branch_id), fault)
251
252
253 class AcquireBranchToPullTestsViaEndpoint(TestCaseWithFactory,
254@@ -499,13 +492,6 @@
255 self.factory = frontend.getLaunchpadObjectFactory()
256 self.branch_lookup = frontend.getBranchLookup()
257
258- def assertFaultEqual(self, expected_fault, observed_fault):
259- """Assert that `expected_fault` equals `observed_fault`."""
260- self.assertIsInstance(observed_fault, faults.LaunchpadFault)
261- self.assertEqual(expected_fault.faultCode, observed_fault.faultCode)
262- self.assertEqual(
263- expected_fault.faultString, observed_fault.faultString)
264-
265 def test_createBranch(self):
266 # createBranch creates a branch with the supplied details and the
267 # caller as registrant.
268@@ -527,7 +513,7 @@
269 path = escape(u'invalid')
270 fault = self.branchfs.createBranch(requester.id, path)
271 login(ANONYMOUS)
272- self.assertFaultEqual(faults.InvalidPath(path), fault)
273+ self.assertEqual(faults.InvalidPath(path), fault)
274
275 def test_createBranch_junk(self):
276 # createBranch can create +junk branches.
277@@ -565,7 +551,7 @@
278 message = "Project 'no-such-product' does not exist."
279 fault = self.branchfs.createBranch(
280 owner.id, escape('/~%s/no-such-product/%s' % (owner.name, name)))
281- self.assertFaultEqual(faults.NotFound(message), fault)
282+ self.assertEqual(faults.NotFound(message), fault)
283
284 def test_createBranch_other_user(self):
285 # Creating a branch under another user's directory fails.
286@@ -578,7 +564,7 @@
287 fault = self.branchfs.createBranch(
288 creator.id,
289 escape('/~%s/%s/%s' % (other_person.name, product.name, name)))
290- self.assertFaultEqual(faults.PermissionDenied(message), fault)
291+ self.assertEqual(faults.PermissionDenied(message), fault)
292
293 def test_createBranch_bad_name(self):
294 # Creating a branch with an invalid name fails.
295@@ -590,7 +576,7 @@
296 fault = self.branchfs.createBranch(
297 owner.id, escape(
298 '/~%s/%s/%s' % (owner.name, product.name, invalid_name)))
299- self.assertFaultEqual(faults.PermissionDenied(message), fault)
300+ self.assertEqual(faults.PermissionDenied(message), fault)
301
302 def test_createBranch_unicode_name(self):
303 # Creating a branch with an invalid name fails.
304@@ -603,7 +589,7 @@
305 fault = self.branchfs.createBranch(
306 owner.id, escape(
307 '/~%s/%s/%s' % (owner.name, product.name, invalid_name)))
308- self.assertFaultEqual(
309+ self.assertEqual(
310 faults.PermissionDenied(message), fault)
311
312 def test_createBranch_bad_user(self):
313@@ -614,7 +600,7 @@
314 message = "User/team 'no-one' does not exist."
315 fault = self.branchfs.createBranch(
316 owner.id, escape('/~no-one/%s/%s' % (product.name, name)))
317- self.assertFaultEqual(faults.NotFound(message), fault)
318+ self.assertEqual(faults.NotFound(message), fault)
319
320 def test_createBranch_bad_user_bad_product(self):
321 # If both the user and the product are not found, then the missing
322@@ -625,7 +611,7 @@
323 message = "User/team 'no-one' does not exist."
324 fault = self.branchfs.createBranch(
325 owner.id, escape('/~no-one/no-product/%s' % (name,)))
326- self.assertFaultEqual(faults.NotFound(message), fault)
327+ self.assertEqual(faults.NotFound(message), fault)
328
329 def test_createBranch_not_branch(self):
330 # Trying to create a branch at a path that's not valid for branches
331@@ -634,7 +620,7 @@
332 path = escape('/~%s' % owner.name)
333 fault = self.branchfs.createBranch(owner.id, path)
334 message = "Cannot create branch at '%s'" % path
335- self.assertFaultEqual(faults.PermissionDenied(message), fault)
336+ self.assertEqual(faults.PermissionDenied(message), fault)
337
338 def test_createBranch_source_package(self):
339 # createBranch can take the path to a source package branch and create
340@@ -671,7 +657,7 @@
341 branch_name)
342 fault = self.branchfs.createBranch(owner.id, escape(unique_name))
343 message = "No such distribution: 'ningnangnong'."
344- self.assertFaultEqual(faults.NotFound(message), fault)
345+ self.assertEqual(faults.NotFound(message), fault)
346
347 def test_createBranch_invalid_distroseries(self):
348 # If createBranch is called with the path to a non-existent
349@@ -685,7 +671,7 @@
350 branch_name)
351 fault = self.branchfs.createBranch(owner.id, escape(unique_name))
352 message = "No such distribution series: 'ningnangnong'."
353- self.assertFaultEqual(faults.NotFound(message), fault)
354+ self.assertEqual(faults.NotFound(message), fault)
355
356 def test_createBranch_invalid_sourcepackagename(self):
357 # If createBranch is called with the path to an invalid source
358@@ -698,7 +684,7 @@
359 branch_name)
360 fault = self.branchfs.createBranch(owner.id, escape(unique_name))
361 message = "No such source package: 'ningnangnong'."
362- self.assertFaultEqual(faults.NotFound(message), fault)
363+ self.assertEqual(faults.NotFound(message), fault)
364
365 def test_initialMirrorRequest(self):
366 # The default 'next_mirror_time' for a newly created hosted branch
367@@ -727,21 +713,21 @@
368 def assertCannotTranslate(self, requester, path):
369 """Assert that we cannot translate 'path'."""
370 fault = self.branchfs.translatePath(requester.id, path)
371- self.assertFaultEqual(faults.PathTranslationError(path), fault)
372+ self.assertEqual(faults.PathTranslationError(path), fault)
373
374 def assertNotFound(self, requester, path):
375 """Assert that the given path cannot be found."""
376 if requester not in [LAUNCHPAD_ANONYMOUS, LAUNCHPAD_SERVICES]:
377 requester = requester.id
378 fault = self.branchfs.translatePath(requester, path)
379- self.assertFaultEqual(faults.PathTranslationError(path), fault)
380+ self.assertEqual(faults.PathTranslationError(path), fault)
381
382 def assertPermissionDenied(self, requester, path):
383 """Assert that looking at the given path gives permission denied."""
384 if requester not in [LAUNCHPAD_ANONYMOUS, LAUNCHPAD_SERVICES]:
385 requester = requester.id
386 fault = self.branchfs.translatePath(requester, path)
387- self.assertFaultEqual(faults.PermissionDenied(), fault)
388+ self.assertEqual(faults.PermissionDenied(), fault)
389
390 def _makeProductWithDevFocus(self, private=False):
391 """Make a stacking-enabled product with a development focus.
392@@ -769,7 +755,7 @@
393 requester = self.factory.makePerson()
394 path = escape(u'invalid')
395 fault = self.branchfs.translatePath(requester.id, path)
396- self.assertFaultEqual(faults.InvalidPath(path), fault)
397+ self.assertEqual(faults.InvalidPath(path), fault)
398
399 def test_translatePath_branch(self):
400 requester = self.factory.makePerson()
401
402=== modified file 'lib/lp/codehosting/sshserver/accesslog.py'
403--- lib/lp/codehosting/sshserver/accesslog.py 2010-03-19 10:43:51 +0000
404+++ lib/lp/codehosting/sshserver/accesslog.py 2010-04-10 13:58:26 +0000
405@@ -29,24 +29,10 @@
406
407 from canonical.config import config
408 from canonical.launchpad.scripts import WatchedFileHandler
409+from lp.services.utils import synchronize
410 from lp.services.twistedsupport.loggingsupport import set_up_oops_reporting
411
412
413-def synchronize(source, target, add, remove):
414- """Update 'source' to match 'target' using 'add' and 'remove'.
415-
416- Changes the container 'source' so that it equals 'target', calling 'add'
417- with any object in 'target' not in 'source' and 'remove' with any object
418- not in 'target' but in 'source'.
419- """
420- need_to_add = [obj for obj in target if obj not in source]
421- need_to_remove = [obj for obj in source if obj not in target]
422- for obj in need_to_add:
423- add(obj)
424- for obj in need_to_remove:
425- remove(obj)
426-
427-
428 class LoggingManager:
429 """Class for managing codehosting logging."""
430
431
432=== modified file 'lib/lp/codehosting/sshserver/auth.py'
433--- lib/lp/codehosting/sshserver/auth.py 2009-06-25 04:06:00 +0000
434+++ lib/lp/codehosting/sshserver/auth.py 2010-04-10 13:58:26 +0000
435@@ -42,7 +42,7 @@
436 from lp.codehosting.sshserver import accesslog
437 from lp.codehosting.sshserver.session import (
438 launch_smart_server, PatchedSSHSession)
439-from lp.codehosting.vfs.branchfsclient import trap_fault
440+from lp.services.twistedsupport.xmlrpc import trap_fault
441 from canonical.config import config
442 from canonical.launchpad.xmlrpc import faults
443
444
445=== modified file 'lib/lp/codehosting/vfs/branchfs.py'
446--- lib/lp/codehosting/vfs/branchfs.py 2010-03-23 03:35:13 +0000
447+++ lib/lp/codehosting/vfs/branchfs.py 2010-04-10 13:58:26 +0000
448@@ -83,16 +83,17 @@
449 from zope.interface import implements, Interface
450
451 from lp.codehosting.vfs.branchfsclient import (
452- BlockingProxy, BranchFileSystemClient, trap_fault)
453+ BlockingProxy, BranchFileSystemClient)
454 from lp.codehosting.vfs.transport import (
455 AsyncVirtualServer, AsyncVirtualTransport, _MultiServer,
456 get_chrooted_transport, get_readonly_transport, TranslationError)
457 from canonical.config import config
458+from canonical.launchpad.xmlrpc import faults
459 from lp.code.enums import BranchType
460 from lp.code.interfaces.branchlookup import IBranchLookup
461 from lp.code.interfaces.codehosting import (
462 BRANCH_TRANSPORT, CONTROL_TRANSPORT, LAUNCHPAD_SERVICES)
463-from canonical.launchpad.xmlrpc import faults
464+from lp.services.twistedsupport.xmlrpc import trap_fault
465
466
467 class BadUrl(Exception):
468
469=== modified file 'lib/lp/codehosting/vfs/branchfsclient.py'
470--- lib/lp/codehosting/vfs/branchfsclient.py 2010-03-23 02:34:35 +0000
471+++ lib/lp/codehosting/vfs/branchfsclient.py 2010-04-10 13:58:26 +0000
472@@ -11,13 +11,11 @@
473 'BlockingProxy',
474 'BranchFileSystemClient',
475 'NotInCache',
476- 'trap_fault',
477 ]
478
479 import time
480
481 from twisted.internet import defer
482-from twisted.web.xmlrpc import Fault
483
484 from lp.code.interfaces.codehosting import BRANCH_TRANSPORT
485
486@@ -136,19 +134,3 @@
487 'translatePath', self._user_id, path)
488 deferred.addCallback(self._addToCache, path)
489 return deferred
490-
491-
492-def trap_fault(failure, *fault_classes):
493- """Trap a fault, based on fault code.
494-
495- :param failure: A Twisted L{Failure}.
496- :param *fault_codes: `LaunchpadFault` subclasses.
497- :raise Failure: if 'failure' is not a Fault failure, or if the fault code
498- does not match the given codes.
499- :return: The Fault if it matches one of the codes.
500- """
501- failure.trap(Fault)
502- fault = failure.value
503- if fault.faultCode in [cls.error_code for cls in fault_classes]:
504- return fault
505- raise failure
506
507=== modified file 'lib/lp/codehosting/vfs/tests/test_branchfsclient.py'
508--- lib/lp/codehosting/vfs/tests/test_branchfsclient.py 2010-03-23 02:34:35 +0000
509+++ lib/lp/codehosting/vfs/tests/test_branchfsclient.py 2010-04-10 13:58:26 +0000
510@@ -9,16 +9,13 @@
511
512 import unittest
513
514-from twisted.python.failure import Failure
515 from twisted.trial.unittest import TestCase
516
517 from lp.codehosting.vfs.branchfsclient import (
518- BranchFileSystemClient, NotInCache, trap_fault)
519+ BranchFileSystemClient, NotInCache)
520 from lp.codehosting.inmemory import InMemoryFrontend, XMLRPCWrapper
521 from lp.code.interfaces.codehosting import BRANCH_TRANSPORT
522 from lp.testing import FakeTime
523-from canonical.launchpad.xmlrpc.tests.test_faults import (
524- TestFaultOne, TestFaultTwo)
525
526
527 class TestBranchFileSystemClient(TestCase):
528@@ -250,56 +247,6 @@
529 [branch1.unique_name, branch2.unique_name], seen_branches)
530
531
532-class TestTrapFault(TestCase):
533- """Tests for `trap_fault`."""
534-
535- def makeFailure(self, exception_factory, *args, **kwargs):
536- """Make a `Failure` from the given exception factory."""
537- try:
538- raise exception_factory(*args, **kwargs)
539- except:
540- return Failure()
541-
542- def assertRaisesFailure(self, failure, function, *args, **kwargs):
543- try:
544- function(*args, **kwargs)
545- except Failure, raised_failure:
546- self.assertEqual(failure, raised_failure)
547-
548- def test_raises_non_faults(self):
549- # trap_fault re-raises any failures it gets that aren't faults.
550- failure = self.makeFailure(RuntimeError, 'example failure')
551- self.assertRaisesFailure(failure, trap_fault, failure, TestFaultOne)
552-
553- def test_raises_faults_with_wrong_code(self):
554- # trap_fault re-raises any failures it gets that are faults but have
555- # the wrong fault code.
556- failure = self.makeFailure(TestFaultOne)
557- self.assertRaisesFailure(failure, trap_fault, failure, TestFaultTwo)
558-
559- def test_raises_faults_if_no_codes_given(self):
560- # If trap_fault is not given any fault codes, it re-raises the fault
561- # failure.
562- failure = self.makeFailure(TestFaultOne)
563- self.assertRaisesFailure(failure, trap_fault, failure)
564-
565- def test_returns_fault_if_code_matches(self):
566- # trap_fault returns the Fault inside the Failure if the fault code
567- # matches what's given.
568- failure = self.makeFailure(TestFaultOne)
569- fault = trap_fault(failure, TestFaultOne)
570- self.assertEqual(TestFaultOne.error_code, fault.faultCode)
571- self.assertEqual(TestFaultOne.msg_template, fault.faultString)
572-
573- def test_returns_fault_if_code_matches_one_of_set(self):
574- # trap_fault returns the Fault inside the Failure if the fault code
575- # matches even one of the given fault codes.
576- failure = self.makeFailure(TestFaultOne)
577- fault = trap_fault(failure, TestFaultOne, TestFaultTwo)
578- self.assertEqual(TestFaultOne.error_code, fault.faultCode)
579- self.assertEqual(TestFaultOne.msg_template, fault.faultString)
580-
581-
582 def test_suite():
583 return unittest.TestLoader().loadTestsFromName(__name__)
584
585
586=== added file 'lib/lp/services/twistedsupport/tests/test_xmlrpc.py'
587--- lib/lp/services/twistedsupport/tests/test_xmlrpc.py 1970-01-01 00:00:00 +0000
588+++ lib/lp/services/twistedsupport/tests/test_xmlrpc.py 2010-04-10 13:58:26 +0000
589@@ -0,0 +1,90 @@
590+# Copyright 2010 Canonical Ltd. This software is licensed under the
591+# GNU Affero General Public License version 3 (see the file LICENSE).
592+
593+"""Tests for Twisted XML-RPC support."""
594+
595+__metaclass__ = type
596+
597+import unittest
598+
599+from twisted.python.failure import Failure
600+from twisted.trial.unittest import TestCase
601+
602+from lp.services.xmlrpc import LaunchpadFault
603+from lp.services.twistedsupport.xmlrpc import trap_fault
604+
605+
606+class TestFaultOne(LaunchpadFault):
607+ """An arbitrary subclass of `LaunchpadFault`.
608+
609+ This class and `TestFaultTwo` are a pair of distinct `LaunchpadFault`
610+ subclasses to use in tests.
611+ """
612+
613+ error_code = 1001
614+ msg_template = "Fault one."
615+
616+
617+class TestFaultTwo(LaunchpadFault):
618+ """Another arbitrary subclass of `LaunchpadFault`.
619+
620+ This class and `TestFaultOne` are a pair of distinct `LaunchpadFault`
621+ subclasses to use in tests.
622+ """
623+
624+ error_code = 1002
625+ msg_template = "Fault two."
626+
627+
628+class TestTrapFault(TestCase):
629+ """Tests for `trap_fault`."""
630+
631+ def makeFailure(self, exception_factory, *args, **kwargs):
632+ """Make a `Failure` from the given exception factory."""
633+ try:
634+ raise exception_factory(*args, **kwargs)
635+ except:
636+ return Failure()
637+
638+ def assertRaisesFailure(self, failure, function, *args, **kwargs):
639+ try:
640+ function(*args, **kwargs)
641+ except Failure, raised_failure:
642+ self.assertEqual(failure, raised_failure)
643+
644+ def test_raises_non_faults(self):
645+ # trap_fault re-raises any failures it gets that aren't faults.
646+ failure = self.makeFailure(RuntimeError, 'example failure')
647+ self.assertRaisesFailure(failure, trap_fault, failure, TestFaultOne)
648+
649+ def test_raises_faults_with_wrong_code(self):
650+ # trap_fault re-raises any failures it gets that are faults but have
651+ # the wrong fault code.
652+ failure = self.makeFailure(TestFaultOne)
653+ self.assertRaisesFailure(failure, trap_fault, failure, TestFaultTwo)
654+
655+ def test_raises_faults_if_no_codes_given(self):
656+ # If trap_fault is not given any fault codes, it re-raises the fault
657+ # failure.
658+ failure = self.makeFailure(TestFaultOne)
659+ self.assertRaisesFailure(failure, trap_fault, failure)
660+
661+ def test_returns_fault_if_code_matches(self):
662+ # trap_fault returns the Fault inside the Failure if the fault code
663+ # matches what's given.
664+ failure = self.makeFailure(TestFaultOne)
665+ fault = trap_fault(failure, TestFaultOne)
666+ self.assertEqual(TestFaultOne.error_code, fault.faultCode)
667+ self.assertEqual(TestFaultOne.msg_template, fault.faultString)
668+
669+ def test_returns_fault_if_code_matches_one_of_set(self):
670+ # trap_fault returns the Fault inside the Failure if the fault code
671+ # matches even one of the given fault codes.
672+ failure = self.makeFailure(TestFaultOne)
673+ fault = trap_fault(failure, TestFaultOne, TestFaultTwo)
674+ self.assertEqual(TestFaultOne.error_code, fault.faultCode)
675+ self.assertEqual(TestFaultOne.msg_template, fault.faultString)
676+
677+
678+def test_suite():
679+ return unittest.TestLoader().loadTestsFromName(__name__)
680
681=== added file 'lib/lp/services/twistedsupport/xmlrpc.py'
682--- lib/lp/services/twistedsupport/xmlrpc.py 1970-01-01 00:00:00 +0000
683+++ lib/lp/services/twistedsupport/xmlrpc.py 2010-04-10 13:58:26 +0000
684@@ -0,0 +1,27 @@
685+# Copyright 2010 Canonical Ltd. This software is licensed under the
686+# GNU Affero General Public License version 3 (see the file LICENSE).
687+
688+"""Support for XML-RPC stuff with Twisted."""
689+
690+__metaclass__ = type
691+__all__ = [
692+ 'trap_fault',
693+ ]
694+
695+from twisted.web.xmlrpc import Fault
696+
697+
698+def trap_fault(failure, *fault_classes):
699+ """Trap a fault, based on fault code.
700+
701+ :param failure: A Twisted L{Failure}.
702+ :param *fault_codes: `LaunchpadFault` subclasses.
703+ :raise Failure: if 'failure' is not a Fault failure, or if the fault code
704+ does not match the given codes.
705+ :return: The Fault if it matches one of the codes.
706+ """
707+ failure.trap(Fault)
708+ fault = failure.value
709+ if fault.faultCode in [cls.error_code for cls in fault_classes]:
710+ return fault
711+ raise failure
712
713=== modified file 'lib/lp/services/utils.py'
714--- lib/lp/services/utils.py 2010-03-18 22:53:50 +0000
715+++ lib/lp/services/utils.py 2010-04-10 13:58:26 +0000
716@@ -1,12 +1,16 @@
717 # Copyright 2009 Canonical Ltd. This software is licensed under the
718 # GNU Affero General Public License version 3 (see the file LICENSE).
719
720+"""Generic Python utilities.
721
722-"""Implementations of the XML-RPC APIs for codehosting."""
723+Functions, lists and so forth. Nothing here that does system calls or network
724+stuff.
725+"""
726
727 __metaclass__ = type
728 __all__ = [
729 'iter_split',
730+ 'synchronize',
731 'text_delta',
732 'value_string',
733 ]
734@@ -35,6 +39,21 @@
735 yield splitter.join(tokens[:i]), splitter.join(tokens[i:])
736
737
738+def synchronize(source, target, add, remove):
739+ """Update 'source' to match 'target' using 'add' and 'remove'.
740+
741+ Changes the container 'source' so that it equals 'target', calling 'add'
742+ with any object in 'target' not in 'source' and 'remove' with any object
743+ not in 'target' but in 'source'.
744+ """
745+ need_to_add = [obj for obj in target if obj not in source]
746+ need_to_remove = [obj for obj in source if obj not in target]
747+ for obj in need_to_add:
748+ add(obj)
749+ for obj in need_to_remove:
750+ remove(obj)
751+
752+
753 def value_string(item):
754 """Return a unicode string representing value.
755
756
757=== added file 'lib/lp/services/xmlrpc.py'
758--- lib/lp/services/xmlrpc.py 1970-01-01 00:00:00 +0000
759+++ lib/lp/services/xmlrpc.py 2010-04-10 13:58:26 +0000
760@@ -0,0 +1,40 @@
761+# Copyright 2010 Canonical Ltd. This software is licensed under the
762+# GNU Affero General Public License version 3 (see the file LICENSE).
763+
764+"""Generic code for XML-RPC in Launchpad."""
765+
766+__metaclass__ = type
767+__all__ = [
768+ 'LaunchpadFault',
769+ ]
770+
771+import xmlrpclib
772+
773+
774+class LaunchpadFault(xmlrpclib.Fault):
775+ """Base class for a Launchpad XMLRPC fault.
776+
777+ Subclasses should define a unique error_code and a msg_template,
778+ which will be interpolated with the given keyword arguments.
779+ """
780+
781+ error_code = None
782+ msg_template = None
783+
784+ def __init__(self, **kw):
785+ assert self.error_code is not None, (
786+ "Subclasses must define error_code.")
787+ assert self.msg_template is not None, (
788+ "Subclasses must define msg_template.")
789+ msg = self.msg_template % kw
790+ xmlrpclib.Fault.__init__(self, self.error_code, msg)
791+
792+ def __eq__(self, other):
793+ if not isinstance(other, LaunchpadFault):
794+ return False
795+ return (
796+ self.faultCode == other.faultCode
797+ and self.faultString == other.faultString)
798+
799+ def __ne__(self, other):
800+ return not (self == other)