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
=== modified file 'doc/conf.py'
--- doc/conf.py 2010-12-09 21:02:11 +0000
+++ doc/conf.py 2011-01-18 14:47:27 +0000
@@ -41,7 +41,7 @@
4141
42# General information about the project.42# General information about the project.
43project = u'piston_mini_client'43project = u'piston_mini_client'
44copyright = u'2010, Anthony Lenton'44copyright = u'2010-2011, Canonical Ltd.'
4545
46# The version info for the project you're documenting, acts as replacement for46# The version info for the project you're documenting, acts as replacement for
47# |version| and |release|, also used in various other places throughout the47# |version| and |release|, also used in various other places throughout the
4848
=== modified file 'doc/quickstart.rst'
--- doc/quickstart.rst 2010-12-09 21:02:11 +0000
+++ doc/quickstart.rst 2011-01-18 14:47:27 +0000
@@ -5,6 +5,7 @@
5your api provides. Each method can specify what arguments it takes, what5your api provides. Each method can specify what arguments it takes, what
6authentication method should be used, and how to process the response.6authentication method should be used, and how to process the response.
77
8=================
8One simple method9One simple method
9=================10=================
1011
@@ -41,10 +42,12 @@
41 level http calls via ``httplib2``.42 level http calls via ``httplib2``.
4243
4344
45======================================
44Validating arguments to your API calls46Validating arguments to your API calls
45======================================47======================================
4648
47If your ``urls.py`` specifies placeholders for resource arguments, as in::49If your server's ``urls.py`` specifies placeholders for resource arguments, as
50in::
4851
49 urlpatterns = patterns('',52 urlpatterns = patterns('',
50 url(r'^foo/(?P<language>[^/]+)/(?P<foobar_id>\d+)/frobble$',53 url(r'^foo/(?P<language>[^/]+)/(?P<foobar_id>\d+)/frobble$',
@@ -92,6 +95,7 @@
9295
93Then again, if we use this we'd need to then ensure that ``foobar_id >= 0``.96Then again, if we use this we'd need to then ensure that ``foobar_id >= 0``.
9497
98==============================================
95Getting back light-weight objects from the API99Getting back light-weight objects from the API
96==============================================100==============================================
97101
@@ -140,3 +144,45 @@
140and specified ``@returns(PistonResponse)`` but it might be nice to be able to144and specified ``@returns(PistonResponse)`` but it might be nice to be able to
141print one of these responses and get a meaningful output, or we might want145print one of these responses and get a meaningful output, or we might want
142to attach some other method to ``FooBarResponse``.146to attach some other method to ``FooBarResponse``.
147
148================
149Handling Failure
150================
151
152A common issue is what to do if the webservice returns an error. One possible
153solution (and the default for ``piston-mini-client``) is to raise an
154exception.
155
156This might not be the best solution for everybody, so piston-mini-client
157allows you to customize the way in which such failures are handled.
158
159You can do this by defining a ``FailHandler`` class. This class needs to
160provide a single method (``handle``), that receives the response headers and
161body, and decides what to do with it all. It can raise an exception, or
162modify the body in any way.
163
164If it returns a string this will be assumed to be the (possibly
165fixed) body of the response and will be deserialized by any decorators the
166method has.
167
168To use a different fail handler in your ``PistonAPI`` set the ``fail_handler``
169class attribute . For example, to use the
170``NoneFailHandler`` instead of the default ``ExceptionFailHandler``,
171you can use::
172
173 class MyAPI(PistonAPI):
174 fail_handler = NoneFailHandler
175 # ... rest of the client definition...
176
177``piston-mini-client`` provides four fail handlers out of the box:
178
179 * ``ExceptionFailHandler``: The default fail handler, raises ``APIError`` if
180 anything goes wrong
181 * ``NoneFailHandler``: Returns None if anything goes wrong. This will
182 provide no information about *what* went wrong, so only use it if you don't
183 really care.
184 * ``DictFailHandler``: If anything goes wrong it returns a dict with all the
185 headers and body for you to debug.
186 * ``MultiExceptionFailHandler``: Raises a different exception according to
187 what went wrong.
188
143189
=== modified file 'piston_mini_client/__init__.py'
--- piston_mini_client/__init__.py 2010-12-22 20:49:37 +0000
+++ piston_mini_client/__init__.py 2011-01-18 14:47:27 +0000
@@ -1,5 +1,5 @@
1# -*- coding: utf-8 -*-1# -*- coding: utf-8 -*-
2# Copyright 2010 Canonical Ltd. This software is licensed under the2# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
3# GNU Lesser General Public License version 3 (see the file LICENSE).3# GNU Lesser General Public License version 3 (see the file LICENSE).
44
5import httplib25import httplib2
@@ -7,17 +7,14 @@
7import urllib7import urllib
8from functools import wraps8from functools import wraps
99
10class APIError(Exception):10from failhandlers import ExceptionFailHandler, APIError
11 def __init__(self, msg, body=None):
12 self.msg = msg
13 self.body = body
14 def __str__(self):
15 return self.msg
1611
17def returns_json(func):12def returns_json(func):
18 @wraps(func)13 @wraps(func)
19 def wrapper(*args, **kwargs):14 def wrapper(*args, **kwargs):
20 response, body = func(*args, **kwargs)15 body = func(*args, **kwargs)
16 if not isinstance(body, basestring):
17 return body
21 return simplejson.loads(body)18 return simplejson.loads(body)
22 return wrapper19 return wrapper
2320
@@ -35,8 +32,10 @@
35 def decorator(func):32 def decorator(func):
36 @wraps(func)33 @wraps(func)
37 def wrapper(self, *args, **kwargs):34 def wrapper(self, *args, **kwargs):
38 response, body = func(self, *args, **kwargs)35 body = func(self, *args, **kwargs)
39 return cls.from_response(response, body, none_allowed)36 if not isinstance(body, basestring):
37 return body
38 return cls.from_response(body, none_allowed)
40 return wrapper39 return wrapper
41 return decorator40 return decorator
4241
@@ -49,7 +48,9 @@
49 def decorator(func):48 def decorator(func):
50 @wraps(func)49 @wraps(func)
51 def wrapper(self, *args, **kwargs):50 def wrapper(self, *args, **kwargs):
52 response, body = func(self, *args, **kwargs)51 body = func(self, *args, **kwargs)
52 if not isinstance(body, basestring):
53 return body
53 data = simplejson.loads(body)54 data = simplejson.loads(body)
54 items = []55 items = []
55 for datum in data:56 for datum in data:
@@ -63,7 +64,7 @@
63 """Base class for objects that are returned from api calls.64 """Base class for objects that are returned from api calls.
64 """65 """
65 @classmethod66 @classmethod
66 def from_response(cls, response, body, none_allowed=False):67 def from_response(cls, body, none_allowed=False):
67 data = simplejson.loads(body)68 data = simplejson.loads(body)
68 if none_allowed and data is None:69 if none_allowed and data is None:
69 return data70 return data
@@ -113,6 +114,8 @@
113114
114 default_content_type = 'application/json'115 default_content_type = 'application/json'
115116
117 fail_handler = ExceptionFailHandler
118
116 def __init__(self, service_root=None, cachedir=None, auth=None):119 def __init__(self, service_root=None, cachedir=None, auth=None):
117 if service_root is None:120 if service_root is None:
118 service_root = self.default_service_root121 service_root = self.default_service_root
@@ -182,9 +185,9 @@
182 raise APIError('Unable to connect to %s' % self._service_root)185 raise APIError('Unable to connect to %s' % self._service_root)
183 else:186 else:
184 raise187 raise
185 if response['status'] not in ['200', '201', '304']:188 handler = self.fail_handler(url, method, body, headers)
186 raise APIError('%s: %s' % (response['status'],response), body)189 body = handler.handle(response, body)
187 return response, body190 return body
188191
189 def _path2url(self, path):192 def _path2url(self, path):
190 return self._service_root.rstrip('/') + '/' + path.lstrip('/')193 return self._service_root.rstrip('/') + '/' + path.lstrip('/')
191194
=== modified file 'piston_mini_client/auth.py'
--- piston_mini_client/auth.py 2010-12-09 17:06:20 +0000
+++ piston_mini_client/auth.py 2011-01-18 14:47:27 +0000
@@ -1,5 +1,5 @@
1# -*- coding: utf-8 -*-1# -*- coding: utf-8 -*-
2# Copyright 2010 Canonical Ltd. This software is licensed under the2# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
3# GNU Lesser General Public License version 3 (see the file LICENSE).3# GNU Lesser General Public License version 3 (see the file LICENSE).
44
5from functools import wraps5from functools import wraps
66
=== added file 'piston_mini_client/failhandlers.py'
--- piston_mini_client/failhandlers.py 1970-01-01 00:00:00 +0000
+++ piston_mini_client/failhandlers.py 2011-01-18 14:47:27 +0000
@@ -0,0 +1,102 @@
1# -*- coding: utf-8 -*-
2# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
3# GNU Lesser General Public License version 3 (see the file LICENSE).
4
5"""A fail handler is passed the raw httplib2 response and body, and has a
6chance to raise an exception, modify the body or return it unaltered, or
7even return a completely different object. It's up to the client (and
8possibly decorators) to know what to do with these returned objects.
9"""
10
11class APIError(Exception):
12 def __init__(self, msg, body=None):
13 self.msg = msg
14 self.body = body
15 def __str__(self):
16 return self.msg
17
18
19class BaseFailHandler(object):
20 """A base class for fail handlers.
21
22 Child classes should at least define handle()
23 """
24 success_status_codes = ['200', '201', '304']
25
26 def __init__(self, url, method, body, headers):
27 """Don't store any of the provided information as we don't need it"""
28 pass
29
30 def handle(self, response, body):
31 raise NotImplementedError()
32
33 def was_error(self, response):
34 """Returns True if 'response' is a failure"""
35 return response.get('status') not in self.success_status_codes
36
37class ExceptionFailHandler(BaseFailHandler):
38 """A fail handler that will raise APIErrors if anything goes wrong"""
39
40 def handle(self, response, body):
41 """Raise APIError if a strange status code is found"""
42 if 'status' not in response:
43 raise APIError('No status code in response')
44 if self.was_error(response):
45 raise APIError('%s: %s' % (response['status'], response), body)
46 return body
47
48
49class NoneFailHandler(BaseFailHandler):
50 """A fail handler that returns None if anything goes wrong.
51
52 You probably only want to use this if you really don't care about what
53 went wrong.
54 """
55 def handle(self, response, body):
56 """Raise APIError if a strange status code is found"""
57 if self.was_error(response):
58 return None
59 return body
60
61
62class DictFailHandler(BaseFailHandler):
63 """A fail handler that returns error information in a dict"""
64
65 def handle(self, response, body):
66 if self.was_error(response):
67 return {'response': response, 'body': body}
68 return body
69
70
71class BadRequestError(APIError):
72 """A 400 Bad Request response was received"""
73
74
75class UnauthorizedError(APIError):
76 """A 401 Bad Request response was received"""
77
78
79class NotFoundError(APIError):
80 """A 404 Not Found response was received"""
81
82
83class InternalServerErrorError(APIError):
84 """A 500 Internal Server Error response was received"""
85
86
87class MultiExceptionFailHandler(BaseFailHandler):
88 """A fail handler that raises an exception according to what goes wrong"""
89 exceptions = {
90 '400': BadRequestError,
91 '401': UnauthorizedError,
92 '404': NotFoundError,
93 '500': InternalServerErrorError,
94 }
95
96 def handle(self, response, body):
97 if self.was_error(response):
98 status = response.get('status')
99 exception_class = self.exceptions.get(status, APIError)
100 raise exception_class('%s: %s' % (status, response), body)
101 return body
102
0103
=== modified file 'piston_mini_client/serializers.py'
--- piston_mini_client/serializers.py 2010-12-09 21:02:11 +0000
+++ piston_mini_client/serializers.py 2011-01-18 14:47:27 +0000
@@ -1,5 +1,5 @@
1# -*- coding: utf-8 -*-1# -*- coding: utf-8 -*-
2# Copyright 2010 Canonical Ltd. This software is licensed under the2# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
3# GNU Lesser General Public License version 3 (see the file LICENSE).3# GNU Lesser General Public License version 3 (see the file LICENSE).
44
5import simplejson5import simplejson
66
=== modified file 'piston_mini_client/test/test_auth.py'
--- piston_mini_client/test/test_auth.py 2010-12-09 21:02:11 +0000
+++ piston_mini_client/test/test_auth.py 2011-01-18 14:47:27 +0000
@@ -1,3 +1,7 @@
1# -*- coding: utf-8 -*-
2# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
3# GNU Lesser General Public License version 3 (see the file LICENSE).
4
1from piston_mini_client.auth import OAuthAuthorizer, BasicAuthorizer5from piston_mini_client.auth import OAuthAuthorizer, BasicAuthorizer
2from unittest import TestCase6from unittest import TestCase
37
48
=== added file 'piston_mini_client/test/test_failhandlers.py'
--- piston_mini_client/test/test_failhandlers.py 1970-01-01 00:00:00 +0000
+++ piston_mini_client/test/test_failhandlers.py 2011-01-18 14:47:27 +0000
@@ -0,0 +1,254 @@
1# -*- coding: utf-8 -*-
2# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
3# GNU Lesser General Public License version 3 (see the file LICENSE).
4
5from mock import patch
6from unittest import TestCase
7from piston_mini_client import (
8 PistonAPI,
9 PistonResponseObject,
10 returns,
11 returns_json,
12 returns_list_of,
13)
14from piston_mini_client.failhandlers import (
15 APIError,
16 BadRequestError,
17 DictFailHandler,
18 ExceptionFailHandler,
19 InternalServerErrorError,
20 MultiExceptionFailHandler,
21 NoneFailHandler,
22 NotFoundError,
23 UnauthorizedError,
24)
25
26class GardeningAPI(PistonAPI):
27 """Just a dummy API so we can play around with"""
28 fail_handler = NoneFailHandler
29 default_service_root = 'http://localhost:12345'
30
31 @returns_json
32 def grow(self):
33 return self._post('/grow', {'plants': 'all'})
34
35 @returns(PistonResponseObject)
36 def get_plant(self):
37 return self._get('/plant')
38
39 @returns_list_of(PistonResponseObject)
40 def get_plants(self):
41 return self._get('/plant')
42
43
44class ExceptionFailHandlerTestCase(TestCase):
45 """As this is the default fail handler, we can skip most tests"""
46 def test_no_status(self):
47 """Check that an exception is raised if no status in response"""
48 handler = ExceptionFailHandler('/foo', 'GET', '', {})
49 self.assertRaises(APIError, handler.handle, {}, '')
50
51 def test_bad_status_codes(self):
52 """Check that APIError is raised if bad status codes are returned"""
53 bad_status = ['404', '500', '401']
54 handler = ExceptionFailHandler('/foo', 'GET', '', {})
55 for status in bad_status:
56 self.assertRaises(APIError, handler.handle,
57 {'status': status}, '')
58
59
60class NoneFailHandlerTestCase(TestCase):
61 def test_no_status(self):
62 handler = NoneFailHandler('/foo', 'GET', '', {})
63 self.assertEqual(None, handler.handle({}, 'not None'))
64
65 def test_bad_status_codes(self):
66 """Check that None is returned if bad status codes are returned"""
67 bad_status = ['404', '500', '401']
68 handler = NoneFailHandler('/foo', 'GET', '', {})
69 for status in bad_status:
70 self.assertEqual(None, handler.handle({'status': status}, ''))
71
72 @patch('httplib2.Http.request')
73 def test_interacts_well_with_returns_json_on_fail(self, mock_request):
74 """Check that NoneFailHandler interacts well with returns_json"""
75 mock_request.return_value = {'status': '500'}, 'invalid json'
76 api = GardeningAPI()
77
78 self.assertEqual(None, api.grow())
79
80 @patch('httplib2.Http.request')
81 def test_interacts_well_with_returns_on_fail(self, mock_request):
82 """Check that NoneFailHandler interacts well with returns"""
83 mock_request.return_value = {'status': '500'}, 'invalid json'
84 api = GardeningAPI()
85
86 self.assertEqual(None, api.get_plant())
87
88 @patch('httplib2.Http.request')
89 def test_interacts_well_with_returns_list_of_on_fail(self, mock_request):
90 """Check that NoneFailHandler interacts well with returns_list_of"""
91 mock_request.return_value = {'status': '500'}, 'invalid json'
92 api = GardeningAPI()
93
94 self.assertEqual(None, api.get_plants())
95
96 @patch('httplib2.Http.request')
97 def test_interacts_well_with_returns_json(self, mock_request):
98 """Check that NoneFailHandler interacts well with returns_json"""
99 mock_request.return_value = {'status': '200'}, '{"foo": "bar"}'
100 api = GardeningAPI()
101
102 self.assertEqual({'foo': 'bar'}, api.grow())
103
104 @patch('httplib2.Http.request')
105 def test_interacts_well_with_returns(self, mock_request):
106 """Check that NoneFailHandler interacts well with returns"""
107 mock_request.return_value = {'status': '200'}, '{"foo": "bar"}'
108 api = GardeningAPI()
109
110 self.assertTrue(isinstance(api.get_plant(), PistonResponseObject))
111
112 @patch('httplib2.Http.request')
113 def test_interacts_well_with_returns_list_of(self, mock_request):
114 """Check that NoneFailHandler interacts well with returns_list_of"""
115 mock_request.return_value = {'status': '200'}, '[]'
116 api = GardeningAPI()
117
118 self.assertEqual([], api.get_plants())
119
120
121class DictFailHandlerTestCase(TestCase):
122 def setUp(self):
123 self.response = {'status': '500'}
124 self.body = 'invalid json'
125 self.expected = {'response': self.response, 'body': self.body}
126 self.api = GardeningAPI()
127 self.api.fail_handler = DictFailHandler
128
129 def test_no_status(self):
130 handler = DictFailHandler('/foo', 'GET', '', {})
131 del self.response['status']
132
133 self.assertEqual(self.expected, handler.handle({}, self.body))
134
135 def test_bad_status_codes(self):
136 bad_status = ['404', '500', '401']
137 handler = DictFailHandler('/foo', 'GET', '', {})
138 for status in bad_status:
139 self.response['status'] = status
140 self.assertEqual(self.expected, handler.handle(**self.expected))
141
142 @patch('httplib2.Http.request')
143 def test_interacts_well_with_returns_json_on_fail(self, mock_request):
144 """Check that DictFailHandler interacts well with returns_json"""
145 mock_request.return_value = self.response, self.body
146
147 self.assertEqual(self.expected, self.api.grow())
148
149 @patch('httplib2.Http.request')
150 def test_interacts_well_with_returns_on_fail(self, mock_request):
151 """Check that NoneFailHandler interacts well with returns"""
152 mock_request.return_value = self.response, self.body
153
154 self.assertEqual(self.expected, self.api.get_plant())
155
156 @patch('httplib2.Http.request')
157 def test_interacts_well_with_returns_list_of_on_fail(self, mock_request):
158 """Check that NoneFailHandler interacts well with returns_list_of"""
159 mock_request.return_value = self.response, self.body
160
161 self.assertEqual(self.expected, self.api.get_plants())
162
163 @patch('httplib2.Http.request')
164 def test_interacts_well_with_returns_json(self, mock_request):
165 """Check that NoneFailHandler interacts well with returns_json"""
166 mock_request.return_value = {'status': '200'}, '{"foo": "bar"}'
167
168 self.assertEqual({'foo': 'bar'}, self.api.grow())
169
170 @patch('httplib2.Http.request')
171 def test_interacts_well_with_returns(self, mock_request):
172 """Check that NoneFailHandler interacts well with returns"""
173 mock_request.return_value = {'status': '200'}, '{"foo": "bar"}'
174
175 self.assertTrue(isinstance(self.api.get_plant(),
176 PistonResponseObject))
177
178 @patch('httplib2.Http.request')
179 def test_interacts_well_with_returns_list_of(self, mock_request):
180 """Check that NoneFailHandler interacts well with returns_list_of"""
181 mock_request.return_value = {'status': '200'}, '[]'
182
183 self.assertEqual([], self.api.get_plants())
184
185
186class MultiExceptionFailHandlerTestCase(TestCase):
187 def setUp(self):
188 self.api = GardeningAPI()
189 self.api.fail_handler = MultiExceptionFailHandler
190
191 def test_no_status(self):
192 handler = MultiExceptionFailHandler('/foo', 'GET', '', {})
193
194 self.assertRaises(APIError, handler.handle, {}, '')
195
196 def test_bad_status_codes(self):
197 bad_status = {
198 '400': BadRequestError,
199 '401': UnauthorizedError,
200 '404': NotFoundError,
201 '500': InternalServerErrorError,
202 }
203 handler = MultiExceptionFailHandler('/foo', 'GET', '', {})
204 for status, exception in bad_status.items():
205 self.assertRaises(exception, handler.handle, {'status': status},
206 '')
207
208 @patch('httplib2.Http.request')
209 def test_interacts_well_with_returns_json_on_fail(self, mock_request):
210 """ Check that MultiExceptionFailHandler interacts well with
211 returns_json"""
212 mock_request.return_value = {'status': '401'}, ''
213
214 self.assertRaises(UnauthorizedError, self.api.grow)
215
216 @patch('httplib2.Http.request')
217 def test_interacts_well_with_returns_on_fail(self, mock_request):
218 """Check that MultiExceptionFailHandler interacts well with returns"""
219 mock_request.return_value = {'status': '404'}, ''
220
221 self.assertRaises(NotFoundError, self.api.get_plant)
222
223 @patch('httplib2.Http.request')
224 def test_interacts_well_with_returns_list_of_on_fail(self, mock_request):
225 """ Check that MultiExceptionFailHandler interacts well with
226 returns_list_of"""
227 mock_request.return_value = {'status': '500'}, ''
228
229 self.assertRaises(InternalServerErrorError, self.api.get_plants)
230
231 @patch('httplib2.Http.request')
232 def test_interacts_well_with_returns_json(self, mock_request):
233 """ Check that MultiExceptionFailHandler interacts well with
234 returns_json"""
235 mock_request.return_value = {'status': '200'}, '{"foo": "bar"}'
236
237 self.assertEqual({'foo': 'bar'}, self.api.grow())
238
239 @patch('httplib2.Http.request')
240 def test_interacts_well_with_returns(self, mock_request):
241 """Check that MultiExceptionFailHandler interacts well with returns"""
242 mock_request.return_value = {'status': '200'}, '{"foo": "bar"}'
243
244 self.assertTrue(isinstance(self.api.get_plant(),
245 PistonResponseObject))
246
247 @patch('httplib2.Http.request')
248 def test_interacts_well_with_returns_list_of(self, mock_request):
249 """ Check that MultiExceptionFailHandler interacts well with
250 returns_list_of"""
251 mock_request.return_value = {'status': '200'}, '[]'
252
253 self.assertEqual([], self.api.get_plants())
254
0255
=== modified file 'piston_mini_client/test/test_resource.py'
--- piston_mini_client/test/test_resource.py 2010-12-21 13:47:11 +0000
+++ piston_mini_client/test/test_resource.py 2011-01-18 14:47:27 +0000
@@ -1,3 +1,7 @@
1# -*- coding: utf-8 -*-
2# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
3# GNU Lesser General Public License version 3 (see the file LICENSE).
4
1from mock import patch5from mock import patch
2from unittest import TestCase6from unittest import TestCase
3from wsgi_intercept import add_wsgi_intercept, remove_wsgi_intercept7from wsgi_intercept import add_wsgi_intercept, remove_wsgi_intercept
@@ -79,12 +83,12 @@
79 @patch('httplib2.Http.request')83 @patch('httplib2.Http.request')
80 def test_valid_status_codes_dont_raise_exception(self, mock_request):84 def test_valid_status_codes_dont_raise_exception(self, mock_request):
81 for status in ['200', '201', '304']:85 for status in ['200', '201', '304']:
82 expected_response = {'status': status}86 response = {'status': status}
83 expected_body = '"hello world!"'87 expected_body = '"hello world!"'
84 mock_request.return_value = (expected_response, expected_body)88 mock_request.return_value = (response, expected_body)
85 api = self.CoffeeAPI()89 api = self.CoffeeAPI()
86 response, body = api._get('/simmer')90 body = api._get('/simmer')
87 self.assertEqual(expected_response, response)91 self.assertEqual(expected_body, body)
8892
89 def test_get_with_extra_args(self):93 def test_get_with_extra_args(self):
90 api = self.CoffeeAPI()94 api = self.CoffeeAPI()
@@ -107,7 +111,7 @@
107111
108class PistonResponseObjectTestCase(TestCase):112class PistonResponseObjectTestCase(TestCase):
109 def test_from_response(self):113 def test_from_response(self):
110 obj = PistonResponseObject.from_response({}, '{"foo": "bar"}')114 obj = PistonResponseObject.from_response('{"foo": "bar"}')
111 self.assertEqual('bar', obj.foo)115 self.assertEqual('bar', obj.foo)
112116
113 def test_from_dict(self):117 def test_from_dict(self):
@@ -121,7 +125,7 @@
121 default_service_root = 'foo'125 default_service_root = 'foo'
122 @returns_json126 @returns_json
123 def func(self):127 def func(self):
124 return {}, '{"foo": "bar", "baz": 42}'128 return '{"foo": "bar", "baz": 42}'
125129
126 result = MyAPI().func()130 result = MyAPI().func()
127 self.assertEqual({"foo": "bar", "baz": 42}, result)131 self.assertEqual({"foo": "bar", "baz": 42}, result)
@@ -133,7 +137,7 @@
133 default_service_root = 'foo'137 default_service_root = 'foo'
134 @returns(PistonResponseObject)138 @returns(PistonResponseObject)
135 def func(self):139 def func(self):
136 return {}, '{"foo": "bar", "baz": 42}'140 return '{"foo": "bar", "baz": 42}'
137141
138 result = MyAPI().func()142 result = MyAPI().func()
139 self.assertTrue(isinstance(result, PistonResponseObject))143 self.assertTrue(isinstance(result, PistonResponseObject))
@@ -143,7 +147,7 @@
143 default_service_root = 'foo'147 default_service_root = 'foo'
144 @returns(PistonResponseObject, none_allowed=True)148 @returns(PistonResponseObject, none_allowed=True)
145 def func(self):149 def func(self):
146 return {}, 'null'150 return 'null'
147151
148 result = MyAPI().func()152 result = MyAPI().func()
149 self.assertEqual(result, None)153 self.assertEqual(result, None)
@@ -153,7 +157,7 @@
153 default_service_root = 'foo'157 default_service_root = 'foo'
154 @returns(PistonResponseObject, none_allowed=True)158 @returns(PistonResponseObject, none_allowed=True)
155 def func(self):159 def func(self):
156 return {}, '{"foo": "bar", "baz": 42}'160 return '{"foo": "bar", "baz": 42}'
157161
158 result = MyAPI().func()162 result = MyAPI().func()
159 self.assertTrue(isinstance(result, PistonResponseObject))163 self.assertTrue(isinstance(result, PistonResponseObject))
@@ -165,7 +169,7 @@
165 default_service_root = 'foo'169 default_service_root = 'foo'
166 @returns_list_of(PistonResponseObject)170 @returns_list_of(PistonResponseObject)
167 def func(self):171 def func(self):
168 return {}, '[{"foo": "bar"}, {"baz": 42}]'172 return '[{"foo": "bar"}, {"baz": 42}]'
169173
170 result = MyAPI().func()174 result = MyAPI().func()
171 self.assertEqual(2, len(result))175 self.assertEqual(2, len(result))
172176
=== modified file 'piston_mini_client/test/test_serializers.py'
--- piston_mini_client/test/test_serializers.py 2010-12-09 21:02:11 +0000
+++ piston_mini_client/test/test_serializers.py 2011-01-18 14:47:27 +0000
@@ -1,3 +1,7 @@
1# -*- coding: utf-8 -*-
2# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
3# GNU Lesser General Public License version 3 (see the file LICENSE).
4
1from unittest import TestCase5from unittest import TestCase
26
3from piston_mini_client.serializers import JSONSerializer, FormSerializer7from piston_mini_client.serializers import JSONSerializer, FormSerializer
48
=== modified file 'piston_mini_client/test/test_validators.py'
--- piston_mini_client/test/test_validators.py 2010-12-09 21:02:11 +0000
+++ piston_mini_client/test/test_validators.py 2011-01-18 14:47:27 +0000
@@ -1,4 +1,7 @@
1# nosetests --with-coverage --cover-html --cover-package=piston_mini_client1# -*- coding: utf-8 -*-
2# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
3# GNU Lesser General Public License version 3 (see the file LICENSE).
4
2from piston_mini_client.validators import (validate_pattern, validate,5from piston_mini_client.validators import (validate_pattern, validate,
3 validate_integer, oauth_protected, basic_protected, ValidationException)6 validate_integer, oauth_protected, basic_protected, ValidationException)
4from piston_mini_client.auth import OAuthAuthorizer, BasicAuthorizer7from piston_mini_client.auth import OAuthAuthorizer, BasicAuthorizer
58
=== modified file 'piston_mini_client/validators.py'
--- piston_mini_client/validators.py 2010-12-09 17:06:20 +0000
+++ piston_mini_client/validators.py 2011-01-18 14:47:27 +0000
@@ -1,5 +1,5 @@
1# -*- coding: utf-8 -*-1# -*- coding: utf-8 -*-
2# Copyright 2010 Canonical Ltd. This software is licensed under the2# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
3# GNU Lesser General Public License version 3 (see the file LICENSE).3# GNU Lesser General Public License version 3 (see the file LICENSE).
44
5""" This module implements a bunch of decorators """5""" This module implements a bunch of decorators """

Subscribers

People subscribed via source and target branches