Revision c6da1ccc
b/kamaki/clients/test/pithos.py | ||
---|---|---|
32 | 32 |
# or implied, of GRNET S.A. |
33 | 33 |
|
34 | 34 |
from unittest import TestCase |
35 |
from mock import patch, call, Mock
|
|
35 |
from mock import patch, call |
|
36 | 36 |
from tempfile import NamedTemporaryFile |
37 | 37 |
from os import urandom |
38 | 38 |
|
... | ... | |
132 | 132 |
"bfe306dd24e92a8d85caf7055643f250fd319e8c4cdd4755ddabbf3ff97e83c7"]) |
133 | 133 |
|
134 | 134 |
|
135 |
class Pithos(TestCase): |
|
135 |
class FR(object): |
|
136 |
"""FR stands for Fake Response""" |
|
137 |
json = dict() |
|
138 |
headers = dict() |
|
139 |
content = json |
|
140 |
status = None |
|
141 |
status_code = 200 |
|
142 |
|
|
143 |
def release(self): |
|
144 |
pass |
|
136 | 145 |
|
137 |
class FR(object): |
|
138 |
"""FR stands for Fake Response""" |
|
139 |
json = dict() |
|
140 |
headers = dict() |
|
141 |
content = json |
|
142 |
status = None |
|
143 |
status_code = 200 |
|
144 | 146 |
|
145 |
def release(self): |
|
146 |
pass |
|
147 |
class Pithos(TestCase): |
|
147 | 148 |
|
148 | 149 |
files = [] |
149 | 150 |
|
... | ... | |
174 | 175 |
self.client.container = 'c0nt@1n3r_i' |
175 | 176 |
|
176 | 177 |
def tearDown(self): |
177 |
self.FR.headers = dict()
|
|
178 |
self.FR.status_code = 200
|
|
179 |
self.FR.json = dict()
|
|
180 |
self.FR.content = self.FR.json
|
|
178 |
FR.headers = dict() |
|
179 |
FR.status_code = 200 |
|
180 |
FR.json = dict() |
|
181 |
FR.content = FR.json
|
|
181 | 182 |
for f in self.files: |
182 | 183 |
f.close() |
183 | 184 |
|
184 | 185 |
# Pithos+ methods that extend storage API |
185 | 186 |
|
186 |
def test_get_account_info(self): |
|
187 |
self.FR.headers = account_info |
|
188 |
self.FR.status_code = 204 |
|
189 |
with patch.object(C, 'perform_request', return_value=self.FR()): |
|
187 |
@patch('kamaki.clients.Client.set_param') |
|
188 |
def test_get_account_info(self, SP): |
|
189 |
FR.headers = account_info |
|
190 |
FR.status_code = 204 |
|
191 |
with patch.object(C, 'perform_request', return_value=FR()): |
|
190 | 192 |
r = self.client.get_account_info() |
191 | 193 |
self.assertEqual(self.client.http_client.url, self.url) |
192 | 194 |
self.assertEqual(self.client.http_client.path, '/%s' % user_id) |
193 | 195 |
self.assert_dicts_are_equal(r, account_info) |
194 |
PC.set_param = Mock() |
|
195 | 196 |
untils = ['date 1', 'date 2', 'date 3'] |
196 | 197 |
for unt in untils: |
197 | 198 |
r = self.client.get_account_info(until=unt) |
198 | 199 |
self.assert_dicts_are_equal(r, account_info) |
199 | 200 |
for i in range(len(untils)): |
200 | 201 |
self.assertEqual( |
201 |
PC.set_param.mock_calls[i], |
|
202 |
PC.set_param.mock_calls[i + 1],
|
|
202 | 203 |
call('until', untils[i], iff=untils[i])) |
203 |
self.FR.status_code = 401
|
|
204 |
FR.status_code = 401 |
|
204 | 205 |
self.assertRaises(ClientError, self.client.get_account_info) |
205 | 206 |
|
206 |
def test_replace_account_meta(self): |
|
207 |
self.FR.status_code = 202 |
|
207 |
@patch('kamaki.clients.Client.set_header') |
|
208 |
def test_replace_account_meta(self, SH): |
|
209 |
FR.status_code = 202 |
|
208 | 210 |
metas = dict(k1='v1', k2='v2', k3='v3') |
209 |
PC.set_header = Mock() |
|
210 |
with patch.object(C, 'perform_request', return_value=self.FR()): |
|
211 |
with patch.object(C, 'perform_request', return_value=FR()): |
|
211 | 212 |
self.client.replace_account_meta(metas) |
212 | 213 |
self.assertEqual(self.client.http_client.url, self.url) |
213 | 214 |
self.assertEqual(self.client.http_client.path, '/%s' % user_id) |
... | ... | |
217 | 218 |
|
218 | 219 |
def test_del_account_meta(self): |
219 | 220 |
keys = ['k1', 'k2', 'k3'] |
220 |
with patch.object(PC, 'account_post', return_value=self.FR()) as ap:
|
|
221 |
with patch.object(PC, 'account_post', return_value=FR()) as ap: |
|
221 | 222 |
expected = [] |
222 | 223 |
for key in keys: |
223 | 224 |
self.client.del_account_meta(key) |
... | ... | |
225 | 226 |
self.assertEqual(ap.mock_calls, expected) |
226 | 227 |
|
227 | 228 |
def test_create_container(self): |
228 |
self.FR.status_code = 201
|
|
229 |
with patch.object(PC, 'put', return_value=self.FR()) as put:
|
|
229 |
FR.status_code = 201 |
|
230 |
with patch.object(PC, 'put', return_value=FR()) as put: |
|
230 | 231 |
cont = 's0m3c0n731n3r' |
231 | 232 |
self.client.create_container(cont) |
232 | 233 |
expected = [call('/%s/%s' % (user_id, cont), success=(201, 202))] |
233 | 234 |
self.assertEqual(put.mock_calls, expected) |
234 |
self.FR.status_code = 202
|
|
235 |
FR.status_code = 202 |
|
235 | 236 |
self.assertRaises(ClientError, self.client.create_container, cont) |
236 | 237 |
|
237 | 238 |
def test_get_container_info(self): |
238 |
self.FR.headers = container_info
|
|
239 |
with patch.object(PC, 'container_head', return_value=self.FR()) as ch:
|
|
239 |
FR.headers = container_info |
|
240 |
with patch.object(PC, 'container_head', return_value=FR()) as ch: |
|
240 | 241 |
r = self.client.get_container_info() |
241 | 242 |
self.assert_dicts_are_equal(r, container_info) |
242 | 243 |
u = 'some date' |
... | ... | |
244 | 245 |
self.assertEqual(ch.mock_calls, [call(until=None), call(until=u)]) |
245 | 246 |
|
246 | 247 |
def test_delete_container(self): |
247 |
self.FR.status_code = 204
|
|
248 |
with patch.object(PC, 'delete', return_value=self.FR()) as delete:
|
|
248 |
FR.status_code = 204 |
|
249 |
with patch.object(PC, 'delete', return_value=FR()) as delete: |
|
249 | 250 |
cont = 's0m3c0n731n3r' |
250 | 251 |
self.client.delete_container(cont) |
251 |
self.FR.status_code = 404
|
|
252 |
FR.status_code = 404 |
|
252 | 253 |
self.assertRaises(ClientError, self.client.delete_container, cont) |
253 |
self.FR.status_code = 409
|
|
254 |
FR.status_code = 409 |
|
254 | 255 |
self.assertRaises(ClientError, self.client.delete_container, cont) |
255 | 256 |
acall = call('/%s/%s' % (user_id, cont), success=(204, 404, 409)) |
256 | 257 |
self.assertEqual(delete.mock_calls, [acall] * 3) |
257 | 258 |
|
258 | 259 |
def test_list_containers(self): |
259 |
self.FR.json = container_list
|
|
260 |
with patch.object(PC, 'account_get', return_value=self.FR()):
|
|
260 |
FR.json = container_list |
|
261 |
with patch.object(PC, 'account_get', return_value=FR()): |
|
261 | 262 |
r = self.client.list_containers() |
262 | 263 |
for i in range(len(r)): |
263 | 264 |
self.assert_dicts_are_equal(r[i], container_list[i]) |
264 | 265 |
|
265 |
def test_upload_object(self): |
|
266 |
PC.get_container_info = Mock(return_value=container_info) |
|
267 |
PC.container_post = Mock(return_value=self.FR()) |
|
268 |
PC.object_put = Mock(return_value=self.FR()) |
|
266 |
@patch( |
|
267 |
'kamaki.clients.pithos.PithosClient.get_container_info', |
|
268 |
return_value=container_info) |
|
269 |
@patch( |
|
270 |
'kamaki.clients.pithos.PithosClient.container_post', |
|
271 |
return_value=FR()) |
|
272 |
@patch( |
|
273 |
'kamaki.clients.pithos.PithosClient.object_put', |
|
274 |
return_value=FR()) |
|
275 |
def test_upload_object(self, CI, CP, OP): |
|
269 | 276 |
num_of_blocks = 8 |
270 | 277 |
tmpFile = self._create_temp_file(num_of_blocks) |
271 | 278 |
|
... | ... | |
369 | 376 |
for arg, val in kwargs.items(): |
370 | 377 |
self.assertEqual(OP.mock_calls[-2][2][arg], val) |
371 | 378 |
|
372 |
def test_create_object(self):
|
|
373 |
PC.set_header = Mock()
|
|
379 |
@patch('kamaki.clients.Client.set_header')
|
|
380 |
def test_create_object(self, SH):
|
|
374 | 381 |
cont = self.client.container |
375 | 382 |
ctype = 'c0n73n7/typ3' |
376 | 383 |
exp_shd = [ |
... | ... | |
378 | 385 |
call('Content-length', '0'), |
379 | 386 |
call('Content-Type', ctype), call('Content-length', '42')] |
380 | 387 |
exp_put = [call('/%s/%s/%s' % (user_id, cont, obj), success=201)] * 2 |
381 |
with patch.object(PC, 'put', return_value=self.FR()) as put:
|
|
388 |
with patch.object(PC, 'put', return_value=FR()) as put: |
|
382 | 389 |
self.client.create_object(obj) |
383 | 390 |
self.client.create_object(obj, |
384 | 391 |
content_type=ctype, content_length=42) |
385 | 392 |
self.assertEqual(PC.set_header.mock_calls, exp_shd) |
386 | 393 |
self.assertEqual(put.mock_calls, exp_put) |
387 | 394 |
|
388 |
def test_create_directory(self):
|
|
389 |
PC.set_header = Mock()
|
|
395 |
@patch('kamaki.clients.Client.set_header')
|
|
396 |
def test_create_directory(self, SH):
|
|
390 | 397 |
cont = self.client.container |
391 | 398 |
exp_shd = [ |
392 | 399 |
call('Content-Type', 'application/directory'), |
393 | 400 |
call('Content-length', '0')] |
394 | 401 |
exp_put = [call('/%s/%s/%s' % (user_id, cont, obj), success=201)] |
395 |
with patch.object(PC, 'put', return_value=self.FR()) as put:
|
|
402 |
with patch.object(PC, 'put', return_value=FR()) as put: |
|
396 | 403 |
self.client.create_directory(obj) |
397 | 404 |
self.assertEqual(PC.set_header.mock_calls, exp_shd) |
398 | 405 |
self.assertEqual(put.mock_calls, exp_put) |
399 | 406 |
|
400 | 407 |
def test_get_object_info(self): |
401 |
self.FR.headers = object_info
|
|
408 |
FR.headers = object_info |
|
402 | 409 |
version = 'v3r510n' |
403 |
with patch.object(PC, 'object_head', return_value=self.FR()) as head:
|
|
410 |
with patch.object(PC, 'object_head', return_value=FR()) as head: |
|
404 | 411 |
r = self.client.get_object_info(obj) |
405 | 412 |
self.assertEqual(r, object_info) |
406 | 413 |
r = self.client.get_object_info(obj, version=version) |
... | ... | |
429 | 436 |
|
430 | 437 |
def test_del_object_meta(self): |
431 | 438 |
metakey = '50m3m3t4k3y' |
432 |
with patch.object(PC, 'object_post', return_value=self.FR()) as post:
|
|
439 |
with patch.object(PC, 'object_post', return_value=FR()) as post: |
|
433 | 440 |
self.client.del_object_meta(obj, metakey) |
434 | 441 |
self.assertEqual( |
435 | 442 |
post.mock_calls, |
436 | 443 |
[call(obj, update=True, metadata={metakey: ''})]) |
437 | 444 |
|
438 |
def test_replace_object_meta(self):
|
|
439 |
PC.set_header = Mock()
|
|
445 |
@patch('kamaki.clients.Client.set_header')
|
|
446 |
def test_replace_object_meta(self, SH):
|
|
440 | 447 |
metas = dict(k1='new1', k2='new2', k3='new3') |
441 | 448 |
cont = self.client.container |
442 |
with patch.object(PC, 'post', return_value=self.FR()) as post:
|
|
449 |
with patch.object(PC, 'post', return_value=FR()) as post: |
|
443 | 450 |
self.client.replace_object_meta(metas) |
444 | 451 |
self.assertEqual(post.mock_calls, [ |
445 | 452 |
call('/%s/%s' % (user_id, cont), |
... | ... | |
463 | 470 |
content_type=None, |
464 | 471 |
source_version=None, |
465 | 472 |
public=False) |
466 |
with patch.object(PC, 'object_put', return_value=self.FR()) as put:
|
|
473 |
with patch.object(PC, 'object_put', return_value=FR()) as put: |
|
467 | 474 |
self.client.copy_object(src_cont, src_obj, dst_cont) |
468 | 475 |
self.assertEqual(put.mock_calls[-1], expected) |
469 | 476 |
self.client.copy_object(src_cont, src_obj, dst_cont, dst_obj) |
... | ... | |
493 | 500 |
content_type=None, |
494 | 501 |
source_version=None, |
495 | 502 |
public=False) |
496 |
with patch.object(PC, 'object_put', return_value=self.FR()) as put:
|
|
503 |
with patch.object(PC, 'object_put', return_value=FR()) as put: |
|
497 | 504 |
self.client.move_object(src_cont, src_obj, dst_cont) |
498 | 505 |
self.assertEqual(put.mock_calls[-1], expected) |
499 | 506 |
self.client.move_object(src_cont, src_obj, dst_cont, dst_obj) |
... | ... | |
510 | 517 |
|
511 | 518 |
def test_delete_object(self): |
512 | 519 |
cont = self.client.container |
513 |
with patch.object(PC, 'delete', return_value=self.FR()) as delete:
|
|
520 |
with patch.object(PC, 'delete', return_value=FR()) as delete: |
|
514 | 521 |
self.client.delete_object(obj) |
515 | 522 |
self.assertEqual(delete.mock_calls, [ |
516 | 523 |
call('/%s/%s/%s' % (user_id, cont, obj), success=(204, 404))]) |
517 |
self.FR.status_code = 404
|
|
524 |
FR.status_code = 404 |
|
518 | 525 |
self.assertRaises(ClientError, self.client.delete_object, obj) |
519 | 526 |
|
520 |
def test_list_objects(self): |
|
521 |
self.FR.json = object_list |
|
527 |
@patch('kamaki.clients.Client.set_param') |
|
528 |
def test_list_objects(self, SP): |
|
529 |
FR.json = object_list |
|
522 | 530 |
acc = self.client.account |
523 | 531 |
cont = self.client.container |
524 |
PC.set_param = Mock() |
|
525 | 532 |
SP = PC.set_param |
526 |
with patch.object(PC, 'get', return_value=self.FR()) as get:
|
|
533 |
with patch.object(PC, 'get', return_value=FR()) as get: |
|
527 | 534 |
r = self.client.list_objects() |
528 | 535 |
for i in range(len(r)): |
529 | 536 |
self.assert_dicts_are_equal(r[i], object_list[i]) |
530 | 537 |
self.assertEqual(get.mock_calls, [ |
531 | 538 |
call('/%s/%s' % (acc, cont), success=(200, 204, 304, 404))]) |
532 | 539 |
self.assertEqual(SP.mock_calls, [call('format', 'json')]) |
533 |
self.FR.status_code = 304
|
|
540 |
FR.status_code = 304 |
|
534 | 541 |
self.assertEqual(self.client.list_objects(), []) |
535 |
self.FR.status_code = 404
|
|
542 |
FR.status_code = 404 |
|
536 | 543 |
self.assertRaises(ClientError, self.client.list_objects) |
537 | 544 |
|
538 |
def test_list_objects_in_path(self): |
|
539 |
self.FR.json = object_list |
|
545 |
@patch('kamaki.clients.Client.set_param') |
|
546 |
def test_list_objects_in_path(self, SP): |
|
547 |
FR.json = object_list |
|
540 | 548 |
path = '/some/awsome/path' |
541 | 549 |
acc = self.client.account |
542 | 550 |
cont = self.client.container |
543 |
PC.set_param = Mock() |
|
544 | 551 |
SP = PC.set_param |
545 |
with patch.object(PC, 'get', return_value=self.FR()) as get:
|
|
552 |
with patch.object(PC, 'get', return_value=FR()) as get: |
|
546 | 553 |
self.client.list_objects_in_path(path) |
547 | 554 |
self.assertEqual(get.mock_calls, [ |
548 | 555 |
call('/%s/%s' % (acc, cont), success=(200, 204, 404))]) |
549 | 556 |
self.assertEqual(SP.mock_calls, [ |
550 | 557 |
call('format', 'json'), call('path', path)]) |
551 |
self.FR.status_code = 404
|
|
558 |
FR.status_code = 404 |
|
552 | 559 |
self.assertRaises(ClientError, self.client.list_objects) |
553 | 560 |
|
554 | 561 |
# Pithos+ only methods |
... | ... | |
557 | 564 |
with patch.object( |
558 | 565 |
PC, |
559 | 566 |
'container_delete', |
560 |
return_value=self.FR()) as cd:
|
|
567 |
return_value=FR()) as cd: |
|
561 | 568 |
self.client.purge_container() |
562 | 569 |
self.assertTrue('until' in cd.mock_calls[-1][2]) |
563 | 570 |
cont = self.client.container |
... | ... | |
576 | 583 |
content_disposition='some content_disposition', |
577 | 584 |
public=True, |
578 | 585 |
permissions=dict(read=['u1', 'g1', 'u2'], write=['u1'])) |
579 |
with patch.object(PC, 'object_put', return_value=self.FR()) as put:
|
|
586 |
with patch.object(PC, 'object_put', return_value=FR()) as put: |
|
580 | 587 |
self.client.upload_object_unchunked(obj, tmpFile) |
581 | 588 |
self.assertEqual(put.mock_calls[-1][1], (obj,)) |
582 | 589 |
self.assertEqual( |
... | ... | |
608 | 615 |
content_disposition='some content_disposition', |
609 | 616 |
public=True, |
610 | 617 |
sharing=dict(read=['u1', 'g1', 'u2'], write=['u1'])) |
611 |
with patch.object(PC, 'object_put', return_value=self.FR()) as put:
|
|
618 |
with patch.object(PC, 'object_put', return_value=FR()) as put: |
|
612 | 619 |
self.client.create_object_by_manifestation(obj) |
613 | 620 |
expected = dict(content_length=0, manifest=manifest) |
614 | 621 |
for k in kwargs: |
... | ... | |
619 | 626 |
expected['permissions'] = expected.pop('sharing') |
620 | 627 |
self.assertEqual(put.mock_calls[-1], call(obj, **expected)) |
621 | 628 |
|
622 |
def test_download_object(self): |
|
623 |
PC.get_object_hashmap = Mock(return_value=object_hashmap) |
|
629 |
@patch( |
|
630 |
'kamaki.clients.pithos.PithosClient.get_object_hashmap', |
|
631 |
return_value=object_hashmap) |
|
632 |
@patch( |
|
633 |
'kamaki.clients.pithos.PithosClient.object_get', |
|
634 |
return_value=FR()) |
|
635 |
def test_download_object(self, GOH, GET): |
|
624 | 636 |
num_of_blocks = 8 |
625 | 637 |
tmpFile = self._create_temp_file(num_of_blocks) |
626 |
self.FR.content = tmpFile.read(4 * 1024 * 1024)
|
|
638 |
FR.content = tmpFile.read(4 * 1024 * 1024) |
|
627 | 639 |
tmpFile = self._create_temp_file(num_of_blocks) |
628 |
PC.object_get = Mock(return_value=self.FR()) |
|
629 | 640 |
GET = PC.object_get |
630 | 641 |
num_of_blocks = len(object_hashmap['hashes']) |
631 |
|
|
632 | 642 |
kwargs = dict( |
633 | 643 |
resume=True, |
634 | 644 |
version='version', |
... | ... | |
709 | 719 |
self.assertEqual(GET.mock_calls[-1][2][k], v) |
710 | 720 |
|
711 | 721 |
def test_get_object_hashmap(self): |
712 |
self.FR.json = object_hashmap
|
|
722 |
FR.json = object_hashmap |
|
713 | 723 |
for empty in (304, 412): |
714 | 724 |
with patch.object( |
715 | 725 |
PC, 'object_get', |
... | ... | |
731 | 741 |
if_modified_since='some date here', |
732 | 742 |
if_unmodified_since='some date here', |
733 | 743 |
data_range='10-20') |
734 |
with patch.object(PC, 'object_get', return_value=self.FR()) as get:
|
|
744 |
with patch.object(PC, 'object_get', return_value=FR()) as get: |
|
735 | 745 |
r = self.client.get_object_hashmap(obj) |
736 | 746 |
self.assertEqual(r, object_hashmap) |
737 | 747 |
self.assertEqual(get.mock_calls[-1], call(obj, **exp_args)) |
... | ... | |
744 | 754 |
def test_set_account_group(self): |
745 | 755 |
group = 'aU53rGr0up' |
746 | 756 |
usernames = ['u1', 'u2', 'u3'] |
747 |
with patch.object(PC, 'account_post', return_value=self.FR()) as post:
|
|
757 |
with patch.object(PC, 'account_post', return_value=FR()) as post: |
|
748 | 758 |
self.client.set_account_group(group, usernames) |
749 | 759 |
self.assertEqual( |
750 | 760 |
post.mock_calls[-1], |
... | ... | |
752 | 762 |
|
753 | 763 |
def test_del_account_group(self): |
754 | 764 |
group = 'aU53rGr0up' |
755 |
with patch.object(PC, 'account_post', return_value=self.FR()) as post:
|
|
765 |
with patch.object(PC, 'account_post', return_value=FR()) as post: |
|
756 | 766 |
self.client.del_account_group(group) |
757 | 767 |
self.assertEqual( |
758 | 768 |
post.mock_calls[-1], |
... | ... | |
802 | 812 |
|
803 | 813 |
def test_set_account_meta(self): |
804 | 814 |
metas = dict(k1='v1', k2='v2', k3='v3') |
805 |
with patch.object(PC, 'account_post', return_value=self.FR()) as post:
|
|
815 |
with patch.object(PC, 'account_post', return_value=FR()) as post: |
|
806 | 816 |
self.client.set_account_meta(metas) |
807 | 817 |
self.assertEqual( |
808 | 818 |
post.mock_calls[-1], |
... | ... | |
810 | 820 |
|
811 | 821 |
def test_set_account_quota(self): |
812 | 822 |
qu = 1024 |
813 |
with patch.object(PC, 'account_post', return_value=self.FR()) as post:
|
|
823 |
with patch.object(PC, 'account_post', return_value=FR()) as post: |
|
814 | 824 |
self.client.set_account_quota(qu) |
815 | 825 |
self.assertEqual(post.mock_calls[-1], call(update=True, quota=qu)) |
816 | 826 |
|
817 | 827 |
def test_set_account_versioning(self): |
818 | 828 |
vrs = 'n3wV3r51on1ngTyp3' |
819 |
with patch.object(PC, 'account_post', return_value=self.FR()) as post:
|
|
829 |
with patch.object(PC, 'account_post', return_value=FR()) as post: |
|
820 | 830 |
self.client.set_account_versioning(vrs) |
821 | 831 |
self.assertEqual( |
822 | 832 |
post.mock_calls[-1], |
... | ... | |
829 | 839 |
with patch.object( |
830 | 840 |
PC, |
831 | 841 |
'container_delete', |
832 |
return_value=self.FR()) as delete:
|
|
842 |
return_value=FR()) as delete: |
|
833 | 843 |
for kwarg in kwarg_list: |
834 | 844 |
self.client.del_container(**kwarg) |
835 | 845 |
expected = dict(kwarg) |
836 | 846 |
expected['success'] = (204, 404, 409) |
837 | 847 |
self.assertEqual(delete.mock_calls[-1], call(**expected)) |
838 | 848 |
for status_code in (404, 409): |
839 |
self.FR.status_code = status_code
|
|
849 |
FR.status_code = status_code |
|
840 | 850 |
self.assertRaises(ClientError, self.client.del_container) |
841 | 851 |
|
842 | 852 |
def test_get_container_versioning(self): |
Also available in: Unified diff