Merge lp:~elachuni/piston-mini-client/failhandler into lp:piston-mini-client

Proposed by Anthony Lenton
Status: Merged
Approved by: Anthony Lenton
Approved revision: 32
Merged at revision: 24
Proposed branch: lp:~elachuni/piston-mini-client/failhandler
Merge into: lp:piston-mini-client
Diff against target: 699 lines (+451/-31)
12 files modified
doc/conf.py (+1/-1)
doc/quickstart.rst (+47/-1)
piston_mini_client/__init__.py (+18/-15)
piston_mini_client/auth.py (+1/-1)
piston_mini_client/failhandlers.py (+102/-0)
piston_mini_client/serializers.py (+1/-1)
piston_mini_client/test/test_auth.py (+4/-0)
piston_mini_client/test/test_failhandlers.py (+254/-0)
piston_mini_client/test/test_resource.py (+14/-10)
piston_mini_client/test/test_serializers.py (+4/-0)
piston_mini_client/test/test_validators.py (+4/-1)
piston_mini_client/validators.py (+1/-1)
To merge this branch: bzr merge lp:~elachuni/piston-mini-client/failhandler
Reviewer Review Type Date Requested Status
Łukasz Czyżykowski (community) Approve
Łukasz Czyżykowski Pending
software-store-developers Pending
Review via email: mp+46001@code.launchpad.net

Description of the change

Overview
========
This branch adds fail handlers, to decouple the behaviour we provide when something goes wrong with the web service.

Details
=======
PistonAPI now uses a FailHandler class to process the response headers and body, before carrying on with deserialization. The FailHandler is instantiated with all the data from the request (so that it can do custom fail handling depending on the particular request, though none of the provided fail handlers does that atm), and must provide a single method handle(response, body) that can raise an exception, modify the body or return it as is.

Tests and docs included.

To post a comment you must log in.
29. By Anthony Lenton

Couple of small typos in the docs.

30. By Anthony Lenton

Removed duplicate line from tests.

31. By Anthony Lenton

Updated copyright year.

32. By Anthony Lenton

Factored out a bit of repeated code.

Revision history for this message
Łukasz Czyżykowski (lukasz-czyzykowski) wrote :

Looks good

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'doc/conf.py'
2--- doc/conf.py 2010-12-09 21:02:11 +0000
3+++ doc/conf.py 2011-01-18 14:47:27 +0000
4@@ -41,7 +41,7 @@
5
6 # General information about the project.
7 project = u'piston_mini_client'
8-copyright = u'2010, Anthony Lenton'
9+copyright = u'2010-2011, Canonical Ltd.'
10
11 # The version info for the project you're documenting, acts as replacement for
12 # |version| and |release|, also used in various other places throughout the
13
14=== modified file 'doc/quickstart.rst'
15--- doc/quickstart.rst 2010-12-09 21:02:11 +0000
16+++ doc/quickstart.rst 2011-01-18 14:47:27 +0000
17@@ -5,6 +5,7 @@
18 your api provides. Each method can specify what arguments it takes, what
19 authentication method should be used, and how to process the response.
20
21+=================
22 One simple method
23 =================
24
25@@ -41,10 +42,12 @@
26 level http calls via ``httplib2``.
27
28
29+======================================
30 Validating arguments to your API calls
31 ======================================
32
33-If your ``urls.py`` specifies placeholders for resource arguments, as in::
34+If your server's ``urls.py`` specifies placeholders for resource arguments, as
35+in::
36
37 urlpatterns = patterns('',
38 url(r'^foo/(?P<language>[^/]+)/(?P<foobar_id>\d+)/frobble$',
39@@ -92,6 +95,7 @@
40
41 Then again, if we use this we'd need to then ensure that ``foobar_id >= 0``.
42
43+==============================================
44 Getting back light-weight objects from the API
45 ==============================================
46
47@@ -140,3 +144,45 @@
48 and specified ``@returns(PistonResponse)`` but it might be nice to be able to
49 print one of these responses and get a meaningful output, or we might want
50 to attach some other method to ``FooBarResponse``.
51+
52+================
53+Handling Failure
54+================
55+
56+A common issue is what to do if the webservice returns an error. One possible
57+solution (and the default for ``piston-mini-client``) is to raise an
58+exception.
59+
60+This might not be the best solution for everybody, so piston-mini-client
61+allows you to customize the way in which such failures are handled.
62+
63+You can do this by defining a ``FailHandler`` class. This class needs to
64+provide a single method (``handle``), that receives the response headers and
65+body, and decides what to do with it all. It can raise an exception, or
66+modify the body in any way.
67+
68+If it returns a string this will be assumed to be the (possibly
69+fixed) body of the response and will be deserialized by any decorators the
70+method has.
71+
72+To use a different fail handler in your ``PistonAPI`` set the ``fail_handler``
73+class attribute . For example, to use the
74+``NoneFailHandler`` instead of the default ``ExceptionFailHandler``,
75+you can use::
76+
77+ class MyAPI(PistonAPI):
78+ fail_handler = NoneFailHandler
79+ # ... rest of the client definition...
80+
81+``piston-mini-client`` provides four fail handlers out of the box:
82+
83+ * ``ExceptionFailHandler``: The default fail handler, raises ``APIError`` if
84+ anything goes wrong
85+ * ``NoneFailHandler``: Returns None if anything goes wrong. This will
86+ provide no information about *what* went wrong, so only use it if you don't
87+ really care.
88+ * ``DictFailHandler``: If anything goes wrong it returns a dict with all the
89+ headers and body for you to debug.
90+ * ``MultiExceptionFailHandler``: Raises a different exception according to
91+ what went wrong.
92+
93
94=== modified file 'piston_mini_client/__init__.py'
95--- piston_mini_client/__init__.py 2010-12-22 20:49:37 +0000
96+++ piston_mini_client/__init__.py 2011-01-18 14:47:27 +0000
97@@ -1,5 +1,5 @@
98 # -*- coding: utf-8 -*-
99-# Copyright 2010 Canonical Ltd. This software is licensed under the
100+# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
101 # GNU Lesser General Public License version 3 (see the file LICENSE).
102
103 import httplib2
104@@ -7,17 +7,14 @@
105 import urllib
106 from functools import wraps
107
108-class APIError(Exception):
109- def __init__(self, msg, body=None):
110- self.msg = msg
111- self.body = body
112- def __str__(self):
113- return self.msg
114+from failhandlers import ExceptionFailHandler, APIError
115
116 def returns_json(func):
117 @wraps(func)
118 def wrapper(*args, **kwargs):
119- response, body = func(*args, **kwargs)
120+ body = func(*args, **kwargs)
121+ if not isinstance(body, basestring):
122+ return body
123 return simplejson.loads(body)
124 return wrapper
125
126@@ -35,8 +32,10 @@
127 def decorator(func):
128 @wraps(func)
129 def wrapper(self, *args, **kwargs):
130- response, body = func(self, *args, **kwargs)
131- return cls.from_response(response, body, none_allowed)
132+ body = func(self, *args, **kwargs)
133+ if not isinstance(body, basestring):
134+ return body
135+ return cls.from_response(body, none_allowed)
136 return wrapper
137 return decorator
138
139@@ -49,7 +48,9 @@
140 def decorator(func):
141 @wraps(func)
142 def wrapper(self, *args, **kwargs):
143- response, body = func(self, *args, **kwargs)
144+ body = func(self, *args, **kwargs)
145+ if not isinstance(body, basestring):
146+ return body
147 data = simplejson.loads(body)
148 items = []
149 for datum in data:
150@@ -63,7 +64,7 @@
151 """Base class for objects that are returned from api calls.
152 """
153 @classmethod
154- def from_response(cls, response, body, none_allowed=False):
155+ def from_response(cls, body, none_allowed=False):
156 data = simplejson.loads(body)
157 if none_allowed and data is None:
158 return data
159@@ -113,6 +114,8 @@
160
161 default_content_type = 'application/json'
162
163+ fail_handler = ExceptionFailHandler
164+
165 def __init__(self, service_root=None, cachedir=None, auth=None):
166 if service_root is None:
167 service_root = self.default_service_root
168@@ -182,9 +185,9 @@
169 raise APIError('Unable to connect to %s' % self._service_root)
170 else:
171 raise
172- if response['status'] not in ['200', '201', '304']:
173- raise APIError('%s: %s' % (response['status'],response), body)
174- return response, body
175+ handler = self.fail_handler(url, method, body, headers)
176+ body = handler.handle(response, body)
177+ return body
178
179 def _path2url(self, path):
180 return self._service_root.rstrip('/') + '/' + path.lstrip('/')
181
182=== modified file 'piston_mini_client/auth.py'
183--- piston_mini_client/auth.py 2010-12-09 17:06:20 +0000
184+++ piston_mini_client/auth.py 2011-01-18 14:47:27 +0000
185@@ -1,5 +1,5 @@
186 # -*- coding: utf-8 -*-
187-# Copyright 2010 Canonical Ltd. This software is licensed under the
188+# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
189 # GNU Lesser General Public License version 3 (see the file LICENSE).
190
191 from functools import wraps
192
193=== added file 'piston_mini_client/failhandlers.py'
194--- piston_mini_client/failhandlers.py 1970-01-01 00:00:00 +0000
195+++ piston_mini_client/failhandlers.py 2011-01-18 14:47:27 +0000
196@@ -0,0 +1,102 @@
197+# -*- coding: utf-8 -*-
198+# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
199+# GNU Lesser General Public License version 3 (see the file LICENSE).
200+
201+"""A fail handler is passed the raw httplib2 response and body, and has a
202+chance to raise an exception, modify the body or return it unaltered, or
203+even return a completely different object. It's up to the client (and
204+possibly decorators) to know what to do with these returned objects.
205+"""
206+
207+class APIError(Exception):
208+ def __init__(self, msg, body=None):
209+ self.msg = msg
210+ self.body = body
211+ def __str__(self):
212+ return self.msg
213+
214+
215+class BaseFailHandler(object):
216+ """A base class for fail handlers.
217+
218+ Child classes should at least define handle()
219+ """
220+ success_status_codes = ['200', '201', '304']
221+
222+ def __init__(self, url, method, body, headers):
223+ """Don't store any of the provided information as we don't need it"""
224+ pass
225+
226+ def handle(self, response, body):
227+ raise NotImplementedError()
228+
229+ def was_error(self, response):
230+ """Returns True if 'response' is a failure"""
231+ return response.get('status') not in self.success_status_codes
232+
233+class ExceptionFailHandler(BaseFailHandler):
234+ """A fail handler that will raise APIErrors if anything goes wrong"""
235+
236+ def handle(self, response, body):
237+ """Raise APIError if a strange status code is found"""
238+ if 'status' not in response:
239+ raise APIError('No status code in response')
240+ if self.was_error(response):
241+ raise APIError('%s: %s' % (response['status'], response), body)
242+ return body
243+
244+
245+class NoneFailHandler(BaseFailHandler):
246+ """A fail handler that returns None if anything goes wrong.
247+
248+ You probably only want to use this if you really don't care about what
249+ went wrong.
250+ """
251+ def handle(self, response, body):
252+ """Raise APIError if a strange status code is found"""
253+ if self.was_error(response):
254+ return None
255+ return body
256+
257+
258+class DictFailHandler(BaseFailHandler):
259+ """A fail handler that returns error information in a dict"""
260+
261+ def handle(self, response, body):
262+ if self.was_error(response):
263+ return {'response': response, 'body': body}
264+ return body
265+
266+
267+class BadRequestError(APIError):
268+ """A 400 Bad Request response was received"""
269+
270+
271+class UnauthorizedError(APIError):
272+ """A 401 Bad Request response was received"""
273+
274+
275+class NotFoundError(APIError):
276+ """A 404 Not Found response was received"""
277+
278+
279+class InternalServerErrorError(APIError):
280+ """A 500 Internal Server Error response was received"""
281+
282+
283+class MultiExceptionFailHandler(BaseFailHandler):
284+ """A fail handler that raises an exception according to what goes wrong"""
285+ exceptions = {
286+ '400': BadRequestError,
287+ '401': UnauthorizedError,
288+ '404': NotFoundError,
289+ '500': InternalServerErrorError,
290+ }
291+
292+ def handle(self, response, body):
293+ if self.was_error(response):
294+ status = response.get('status')
295+ exception_class = self.exceptions.get(status, APIError)
296+ raise exception_class('%s: %s' % (status, response), body)
297+ return body
298+
299
300=== modified file 'piston_mini_client/serializers.py'
301--- piston_mini_client/serializers.py 2010-12-09 21:02:11 +0000
302+++ piston_mini_client/serializers.py 2011-01-18 14:47:27 +0000
303@@ -1,5 +1,5 @@
304 # -*- coding: utf-8 -*-
305-# Copyright 2010 Canonical Ltd. This software is licensed under the
306+# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
307 # GNU Lesser General Public License version 3 (see the file LICENSE).
308
309 import simplejson
310
311=== modified file 'piston_mini_client/test/test_auth.py'
312--- piston_mini_client/test/test_auth.py 2010-12-09 21:02:11 +0000
313+++ piston_mini_client/test/test_auth.py 2011-01-18 14:47:27 +0000
314@@ -1,3 +1,7 @@
315+# -*- coding: utf-8 -*-
316+# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
317+# GNU Lesser General Public License version 3 (see the file LICENSE).
318+
319 from piston_mini_client.auth import OAuthAuthorizer, BasicAuthorizer
320 from unittest import TestCase
321
322
323=== added file 'piston_mini_client/test/test_failhandlers.py'
324--- piston_mini_client/test/test_failhandlers.py 1970-01-01 00:00:00 +0000
325+++ piston_mini_client/test/test_failhandlers.py 2011-01-18 14:47:27 +0000
326@@ -0,0 +1,254 @@
327+# -*- coding: utf-8 -*-
328+# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
329+# GNU Lesser General Public License version 3 (see the file LICENSE).
330+
331+from mock import patch
332+from unittest import TestCase
333+from piston_mini_client import (
334+ PistonAPI,
335+ PistonResponseObject,
336+ returns,
337+ returns_json,
338+ returns_list_of,
339+)
340+from piston_mini_client.failhandlers import (
341+ APIError,
342+ BadRequestError,
343+ DictFailHandler,
344+ ExceptionFailHandler,
345+ InternalServerErrorError,
346+ MultiExceptionFailHandler,
347+ NoneFailHandler,
348+ NotFoundError,
349+ UnauthorizedError,
350+)
351+
352+class GardeningAPI(PistonAPI):
353+ """Just a dummy API so we can play around with"""
354+ fail_handler = NoneFailHandler
355+ default_service_root = 'http://localhost:12345'
356+
357+ @returns_json
358+ def grow(self):
359+ return self._post('/grow', {'plants': 'all'})
360+
361+ @returns(PistonResponseObject)
362+ def get_plant(self):
363+ return self._get('/plant')
364+
365+ @returns_list_of(PistonResponseObject)
366+ def get_plants(self):
367+ return self._get('/plant')
368+
369+
370+class ExceptionFailHandlerTestCase(TestCase):
371+ """As this is the default fail handler, we can skip most tests"""
372+ def test_no_status(self):
373+ """Check that an exception is raised if no status in response"""
374+ handler = ExceptionFailHandler('/foo', 'GET', '', {})
375+ self.assertRaises(APIError, handler.handle, {}, '')
376+
377+ def test_bad_status_codes(self):
378+ """Check that APIError is raised if bad status codes are returned"""
379+ bad_status = ['404', '500', '401']
380+ handler = ExceptionFailHandler('/foo', 'GET', '', {})
381+ for status in bad_status:
382+ self.assertRaises(APIError, handler.handle,
383+ {'status': status}, '')
384+
385+
386+class NoneFailHandlerTestCase(TestCase):
387+ def test_no_status(self):
388+ handler = NoneFailHandler('/foo', 'GET', '', {})
389+ self.assertEqual(None, handler.handle({}, 'not None'))
390+
391+ def test_bad_status_codes(self):
392+ """Check that None is returned if bad status codes are returned"""
393+ bad_status = ['404', '500', '401']
394+ handler = NoneFailHandler('/foo', 'GET', '', {})
395+ for status in bad_status:
396+ self.assertEqual(None, handler.handle({'status': status}, ''))
397+
398+ @patch('httplib2.Http.request')
399+ def test_interacts_well_with_returns_json_on_fail(self, mock_request):
400+ """Check that NoneFailHandler interacts well with returns_json"""
401+ mock_request.return_value = {'status': '500'}, 'invalid json'
402+ api = GardeningAPI()
403+
404+ self.assertEqual(None, api.grow())
405+
406+ @patch('httplib2.Http.request')
407+ def test_interacts_well_with_returns_on_fail(self, mock_request):
408+ """Check that NoneFailHandler interacts well with returns"""
409+ mock_request.return_value = {'status': '500'}, 'invalid json'
410+ api = GardeningAPI()
411+
412+ self.assertEqual(None, api.get_plant())
413+
414+ @patch('httplib2.Http.request')
415+ def test_interacts_well_with_returns_list_of_on_fail(self, mock_request):
416+ """Check that NoneFailHandler interacts well with returns_list_of"""
417+ mock_request.return_value = {'status': '500'}, 'invalid json'
418+ api = GardeningAPI()
419+
420+ self.assertEqual(None, api.get_plants())
421+
422+ @patch('httplib2.Http.request')
423+ def test_interacts_well_with_returns_json(self, mock_request):
424+ """Check that NoneFailHandler interacts well with returns_json"""
425+ mock_request.return_value = {'status': '200'}, '{"foo": "bar"}'
426+ api = GardeningAPI()
427+
428+ self.assertEqual({'foo': 'bar'}, api.grow())
429+
430+ @patch('httplib2.Http.request')
431+ def test_interacts_well_with_returns(self, mock_request):
432+ """Check that NoneFailHandler interacts well with returns"""
433+ mock_request.return_value = {'status': '200'}, '{"foo": "bar"}'
434+ api = GardeningAPI()
435+
436+ self.assertTrue(isinstance(api.get_plant(), PistonResponseObject))
437+
438+ @patch('httplib2.Http.request')
439+ def test_interacts_well_with_returns_list_of(self, mock_request):
440+ """Check that NoneFailHandler interacts well with returns_list_of"""
441+ mock_request.return_value = {'status': '200'}, '[]'
442+ api = GardeningAPI()
443+
444+ self.assertEqual([], api.get_plants())
445+
446+
447+class DictFailHandlerTestCase(TestCase):
448+ def setUp(self):
449+ self.response = {'status': '500'}
450+ self.body = 'invalid json'
451+ self.expected = {'response': self.response, 'body': self.body}
452+ self.api = GardeningAPI()
453+ self.api.fail_handler = DictFailHandler
454+
455+ def test_no_status(self):
456+ handler = DictFailHandler('/foo', 'GET', '', {})
457+ del self.response['status']
458+
459+ self.assertEqual(self.expected, handler.handle({}, self.body))
460+
461+ def test_bad_status_codes(self):
462+ bad_status = ['404', '500', '401']
463+ handler = DictFailHandler('/foo', 'GET', '', {})
464+ for status in bad_status:
465+ self.response['status'] = status
466+ self.assertEqual(self.expected, handler.handle(**self.expected))
467+
468+ @patch('httplib2.Http.request')
469+ def test_interacts_well_with_returns_json_on_fail(self, mock_request):
470+ """Check that DictFailHandler interacts well with returns_json"""
471+ mock_request.return_value = self.response, self.body
472+
473+ self.assertEqual(self.expected, self.api.grow())
474+
475+ @patch('httplib2.Http.request')
476+ def test_interacts_well_with_returns_on_fail(self, mock_request):
477+ """Check that NoneFailHandler interacts well with returns"""
478+ mock_request.return_value = self.response, self.body
479+
480+ self.assertEqual(self.expected, self.api.get_plant())
481+
482+ @patch('httplib2.Http.request')
483+ def test_interacts_well_with_returns_list_of_on_fail(self, mock_request):
484+ """Check that NoneFailHandler interacts well with returns_list_of"""
485+ mock_request.return_value = self.response, self.body
486+
487+ self.assertEqual(self.expected, self.api.get_plants())
488+
489+ @patch('httplib2.Http.request')
490+ def test_interacts_well_with_returns_json(self, mock_request):
491+ """Check that NoneFailHandler interacts well with returns_json"""
492+ mock_request.return_value = {'status': '200'}, '{"foo": "bar"}'
493+
494+ self.assertEqual({'foo': 'bar'}, self.api.grow())
495+
496+ @patch('httplib2.Http.request')
497+ def test_interacts_well_with_returns(self, mock_request):
498+ """Check that NoneFailHandler interacts well with returns"""
499+ mock_request.return_value = {'status': '200'}, '{"foo": "bar"}'
500+
501+ self.assertTrue(isinstance(self.api.get_plant(),
502+ PistonResponseObject))
503+
504+ @patch('httplib2.Http.request')
505+ def test_interacts_well_with_returns_list_of(self, mock_request):
506+ """Check that NoneFailHandler interacts well with returns_list_of"""
507+ mock_request.return_value = {'status': '200'}, '[]'
508+
509+ self.assertEqual([], self.api.get_plants())
510+
511+
512+class MultiExceptionFailHandlerTestCase(TestCase):
513+ def setUp(self):
514+ self.api = GardeningAPI()
515+ self.api.fail_handler = MultiExceptionFailHandler
516+
517+ def test_no_status(self):
518+ handler = MultiExceptionFailHandler('/foo', 'GET', '', {})
519+
520+ self.assertRaises(APIError, handler.handle, {}, '')
521+
522+ def test_bad_status_codes(self):
523+ bad_status = {
524+ '400': BadRequestError,
525+ '401': UnauthorizedError,
526+ '404': NotFoundError,
527+ '500': InternalServerErrorError,
528+ }
529+ handler = MultiExceptionFailHandler('/foo', 'GET', '', {})
530+ for status, exception in bad_status.items():
531+ self.assertRaises(exception, handler.handle, {'status': status},
532+ '')
533+
534+ @patch('httplib2.Http.request')
535+ def test_interacts_well_with_returns_json_on_fail(self, mock_request):
536+ """ Check that MultiExceptionFailHandler interacts well with
537+ returns_json"""
538+ mock_request.return_value = {'status': '401'}, ''
539+
540+ self.assertRaises(UnauthorizedError, self.api.grow)
541+
542+ @patch('httplib2.Http.request')
543+ def test_interacts_well_with_returns_on_fail(self, mock_request):
544+ """Check that MultiExceptionFailHandler interacts well with returns"""
545+ mock_request.return_value = {'status': '404'}, ''
546+
547+ self.assertRaises(NotFoundError, self.api.get_plant)
548+
549+ @patch('httplib2.Http.request')
550+ def test_interacts_well_with_returns_list_of_on_fail(self, mock_request):
551+ """ Check that MultiExceptionFailHandler interacts well with
552+ returns_list_of"""
553+ mock_request.return_value = {'status': '500'}, ''
554+
555+ self.assertRaises(InternalServerErrorError, self.api.get_plants)
556+
557+ @patch('httplib2.Http.request')
558+ def test_interacts_well_with_returns_json(self, mock_request):
559+ """ Check that MultiExceptionFailHandler interacts well with
560+ returns_json"""
561+ mock_request.return_value = {'status': '200'}, '{"foo": "bar"}'
562+
563+ self.assertEqual({'foo': 'bar'}, self.api.grow())
564+
565+ @patch('httplib2.Http.request')
566+ def test_interacts_well_with_returns(self, mock_request):
567+ """Check that MultiExceptionFailHandler interacts well with returns"""
568+ mock_request.return_value = {'status': '200'}, '{"foo": "bar"}'
569+
570+ self.assertTrue(isinstance(self.api.get_plant(),
571+ PistonResponseObject))
572+
573+ @patch('httplib2.Http.request')
574+ def test_interacts_well_with_returns_list_of(self, mock_request):
575+ """ Check that MultiExceptionFailHandler interacts well with
576+ returns_list_of"""
577+ mock_request.return_value = {'status': '200'}, '[]'
578+
579+ self.assertEqual([], self.api.get_plants())
580+
581
582=== modified file 'piston_mini_client/test/test_resource.py'
583--- piston_mini_client/test/test_resource.py 2010-12-21 13:47:11 +0000
584+++ piston_mini_client/test/test_resource.py 2011-01-18 14:47:27 +0000
585@@ -1,3 +1,7 @@
586+# -*- coding: utf-8 -*-
587+# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
588+# GNU Lesser General Public License version 3 (see the file LICENSE).
589+
590 from mock import patch
591 from unittest import TestCase
592 from wsgi_intercept import add_wsgi_intercept, remove_wsgi_intercept
593@@ -79,12 +83,12 @@
594 @patch('httplib2.Http.request')
595 def test_valid_status_codes_dont_raise_exception(self, mock_request):
596 for status in ['200', '201', '304']:
597- expected_response = {'status': status}
598+ response = {'status': status}
599 expected_body = '"hello world!"'
600- mock_request.return_value = (expected_response, expected_body)
601+ mock_request.return_value = (response, expected_body)
602 api = self.CoffeeAPI()
603- response, body = api._get('/simmer')
604- self.assertEqual(expected_response, response)
605+ body = api._get('/simmer')
606+ self.assertEqual(expected_body, body)
607
608 def test_get_with_extra_args(self):
609 api = self.CoffeeAPI()
610@@ -107,7 +111,7 @@
611
612 class PistonResponseObjectTestCase(TestCase):
613 def test_from_response(self):
614- obj = PistonResponseObject.from_response({}, '{"foo": "bar"}')
615+ obj = PistonResponseObject.from_response('{"foo": "bar"}')
616 self.assertEqual('bar', obj.foo)
617
618 def test_from_dict(self):
619@@ -121,7 +125,7 @@
620 default_service_root = 'foo'
621 @returns_json
622 def func(self):
623- return {}, '{"foo": "bar", "baz": 42}'
624+ return '{"foo": "bar", "baz": 42}'
625
626 result = MyAPI().func()
627 self.assertEqual({"foo": "bar", "baz": 42}, result)
628@@ -133,7 +137,7 @@
629 default_service_root = 'foo'
630 @returns(PistonResponseObject)
631 def func(self):
632- return {}, '{"foo": "bar", "baz": 42}'
633+ return '{"foo": "bar", "baz": 42}'
634
635 result = MyAPI().func()
636 self.assertTrue(isinstance(result, PistonResponseObject))
637@@ -143,7 +147,7 @@
638 default_service_root = 'foo'
639 @returns(PistonResponseObject, none_allowed=True)
640 def func(self):
641- return {}, 'null'
642+ return 'null'
643
644 result = MyAPI().func()
645 self.assertEqual(result, None)
646@@ -153,7 +157,7 @@
647 default_service_root = 'foo'
648 @returns(PistonResponseObject, none_allowed=True)
649 def func(self):
650- return {}, '{"foo": "bar", "baz": 42}'
651+ return '{"foo": "bar", "baz": 42}'
652
653 result = MyAPI().func()
654 self.assertTrue(isinstance(result, PistonResponseObject))
655@@ -165,7 +169,7 @@
656 default_service_root = 'foo'
657 @returns_list_of(PistonResponseObject)
658 def func(self):
659- return {}, '[{"foo": "bar"}, {"baz": 42}]'
660+ return '[{"foo": "bar"}, {"baz": 42}]'
661
662 result = MyAPI().func()
663 self.assertEqual(2, len(result))
664
665=== modified file 'piston_mini_client/test/test_serializers.py'
666--- piston_mini_client/test/test_serializers.py 2010-12-09 21:02:11 +0000
667+++ piston_mini_client/test/test_serializers.py 2011-01-18 14:47:27 +0000
668@@ -1,3 +1,7 @@
669+# -*- coding: utf-8 -*-
670+# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
671+# GNU Lesser General Public License version 3 (see the file LICENSE).
672+
673 from unittest import TestCase
674
675 from piston_mini_client.serializers import JSONSerializer, FormSerializer
676
677=== modified file 'piston_mini_client/test/test_validators.py'
678--- piston_mini_client/test/test_validators.py 2010-12-09 21:02:11 +0000
679+++ piston_mini_client/test/test_validators.py 2011-01-18 14:47:27 +0000
680@@ -1,4 +1,7 @@
681-# nosetests --with-coverage --cover-html --cover-package=piston_mini_client
682+# -*- coding: utf-8 -*-
683+# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
684+# GNU Lesser General Public License version 3 (see the file LICENSE).
685+
686 from piston_mini_client.validators import (validate_pattern, validate,
687 validate_integer, oauth_protected, basic_protected, ValidationException)
688 from piston_mini_client.auth import OAuthAuthorizer, BasicAuthorizer
689
690=== modified file 'piston_mini_client/validators.py'
691--- piston_mini_client/validators.py 2010-12-09 17:06:20 +0000
692+++ piston_mini_client/validators.py 2011-01-18 14:47:27 +0000
693@@ -1,5 +1,5 @@
694 # -*- coding: utf-8 -*-
695-# Copyright 2010 Canonical Ltd. This software is licensed under the
696+# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
697 # GNU Lesser General Public License version 3 (see the file LICENSE).
698
699 """ This module implements a bunch of decorators """

Subscribers

People subscribed via source and target branches