Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aws
GitHub Repository: aws/aws-cli
Path: blob/develop/tests/unit/customizations/test_paginate.py
2627 views
1
# Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
#
3
# Licensed under the Apache License, Version 2.0 (the "License"). You
4
# may not use this file except in compliance with the License. A copy of
5
# the License is located at
6
#
7
# http://aws.amazon.com/apache2.0/
8
#
9
# or in the "license" file accompanying this file. This file is
10
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
# ANY KIND, either express or implied. See the License for the specific
12
# language governing permissions and limitations under the License.
13
from functools import partial
14
15
import pytest
16
17
from awscli.customizations.paginate import PageArgument
18
from awscli.testutils import mock, unittest, capture_output
19
20
from botocore.exceptions import DataNotFoundError, PaginationError
21
from botocore.model import OperationModel
22
from awscli.help import OperationHelpCommand, OperationDocumentEventHandler
23
24
from awscli.customizations import paginate
25
26
@pytest.fixture
27
def max_items_page_arg():
28
return PageArgument('max-items', 'documentation', int, 'MaxItems')
29
30
class TestPaginateBase(unittest.TestCase):
31
32
def setUp(self):
33
self.session = mock.Mock()
34
self.paginator_model = mock.Mock()
35
self.pagination_config = {
36
'input_token': 'Foo',
37
'limit_key': 'Bar',
38
}
39
self.paginator_model.get_paginator.return_value = \
40
self.pagination_config
41
self.session.get_paginator_model.return_value = self.paginator_model
42
43
self.operation_model = mock.Mock()
44
self.foo_param = mock.Mock()
45
self.foo_param.name = 'Foo'
46
self.foo_param.type_name = 'string'
47
self.bar_param = mock.Mock()
48
self.bar_param.type_name = 'string'
49
self.bar_param.name = 'Bar'
50
self.params = [self.foo_param, self.bar_param]
51
self.operation_model.input_shape.members = {"Foo": self.foo_param,
52
"Bar": self.bar_param}
53
54
55
class TestArgumentTableModifications(TestPaginateBase):
56
57
def test_customize_arg_table(self):
58
argument_table = {
59
'foo': mock.Mock(),
60
'bar': mock.Mock(),
61
}
62
paginate.unify_paging_params(argument_table, self.operation_model,
63
'building-argument-table.foo.bar',
64
self.session)
65
# We should mark the built in input_token as 'hidden'.
66
self.assertTrue(argument_table['foo']._UNDOCUMENTED)
67
# Also need to hide the limit key.
68
self.assertTrue(argument_table['bar']._UNDOCUMENTED)
69
# We also need to inject starting-token and max-items.
70
self.assertIn('starting-token', argument_table)
71
self.assertIn('max-items', argument_table)
72
self.assertIn('page-size', argument_table)
73
# And these should be PageArguments.
74
self.assertIsInstance(argument_table['starting-token'],
75
paginate.PageArgument)
76
self.assertIsInstance(argument_table['max-items'],
77
paginate.PageArgument)
78
self.assertIsInstance(argument_table['page-size'],
79
paginate.PageArgument)
80
81
def test_operation_with_no_paginate(self):
82
# Operations that don't paginate are left alone.
83
self.paginator_model.get_paginator.side_effect = ValueError()
84
argument_table = {
85
'foo': 'FakeArgObject',
86
'bar': 'FakeArgObject',
87
}
88
starting_table = argument_table.copy()
89
paginate.unify_paging_params(argument_table, self.operation_model,
90
'building-argument-table.foo.bar',
91
self.session)
92
self.assertEqual(starting_table, argument_table)
93
94
def test_service_with_no_paginate(self):
95
# Operations that don't paginate are left alone.
96
self.session.get_paginator_model.side_effect = \
97
DataNotFoundError(data_path='foo.paginators.json')
98
argument_table = {
99
'foo': 'FakeArgObject',
100
'bar': 'FakeArgObject',
101
}
102
starting_table = argument_table.copy()
103
paginate.unify_paging_params(argument_table, self.operation_model,
104
'building-argument-table.foo.bar',
105
self.session)
106
self.assertEqual(starting_table, argument_table)
107
108
109
class TestHelpDocumentationModifications(TestPaginateBase):
110
def test_injects_pagination_help_text(self):
111
with mock.patch('awscli.customizations.paginate.get_paginator_config',
112
return_value={'result_key': 'abc'}):
113
help_command = OperationHelpCommand(
114
mock.Mock(), mock.Mock(), mock.Mock(), 'foo', OperationDocumentEventHandler)
115
help_command.obj = mock.Mock(OperationModel)
116
help_command.obj.name = 'foo'
117
paginate.add_paging_description(help_command)
118
self.assertIn('``foo`` is a paginated operation. Multiple API',
119
help_command.doc.getvalue().decode())
120
self.assertIn('following query expressions: ``abc``',
121
help_command.doc.getvalue().decode())
122
123
def test_shows_result_keys_when_array(self):
124
with mock.patch('awscli.customizations.paginate.get_paginator_config',
125
return_value={'result_key': ['abc', '123']}):
126
help_command = OperationHelpCommand(
127
mock.Mock(), mock.Mock(), mock.Mock(), 'foo', OperationDocumentEventHandler)
128
help_command.obj = mock.Mock(OperationModel)
129
help_command.obj.name = 'foo'
130
paginate.add_paging_description(help_command)
131
self.assertIn('following query expressions: ``abc``, ``123``',
132
help_command.doc.getvalue().decode())
133
134
def test_does_not_show_result_key_if_not_present(self):
135
with mock.patch('awscli.customizations.paginate.get_paginator_config',
136
return_value={'limit_key': 'aaa'}):
137
help_command = OperationHelpCommand(
138
mock.Mock(), mock.Mock(), mock.Mock(), 'foo', OperationDocumentEventHandler)
139
help_command.obj = mock.Mock(OperationModel)
140
help_command.obj.name = 'foo'
141
paginate.add_paging_description(help_command)
142
self.assertIn('``foo`` is a paginated operation. Multiple API',
143
help_command.doc.getvalue().decode())
144
self.assertNotIn('following query expressions',
145
help_command.doc.getvalue().decode())
146
147
def test_does_not_inject_when_no_pagination(self):
148
with mock.patch('awscli.customizations.paginate.get_paginator_config',
149
return_value=None):
150
help_command = OperationHelpCommand(
151
mock.Mock(), mock.Mock(), mock.Mock(), 'foo', OperationDocumentEventHandler)
152
help_command.obj = mock.Mock(OperationModel)
153
help_command.obj.name = 'foo'
154
paginate.add_paging_description(help_command)
155
self.assertNotIn('``foo`` is a paginated operation',
156
help_command.doc.getvalue().decode())
157
158
159
class TestStringLimitKey(TestPaginateBase):
160
161
def setUp(self):
162
super(TestStringLimitKey, self).setUp()
163
self.bar_param.type_name = 'string'
164
165
def test_integer_limit_key(self):
166
argument_table = {
167
'foo': mock.Mock(),
168
'bar': mock.Mock(),
169
}
170
paginate.unify_paging_params(argument_table, self.operation_model,
171
'building-argument-table.foo.bar',
172
self.session)
173
# Max items should be the same type as bar, which may not be an int
174
self.assertEqual('string', argument_table['max-items'].cli_type_name)
175
176
177
class TestIntegerLimitKey(TestPaginateBase):
178
179
def setUp(self):
180
super(TestIntegerLimitKey, self).setUp()
181
self.bar_param.type_name = 'integer'
182
183
def test_integer_limit_key(self):
184
argument_table = {
185
'foo': mock.Mock(),
186
'bar': mock.Mock(),
187
}
188
paginate.unify_paging_params(argument_table, self.operation_model,
189
'building-argument-table.foo.bar',
190
self.session)
191
# Max items should be the same type as bar, which may not be an int
192
self.assertEqual('integer', argument_table['max-items'].cli_type_name)
193
194
195
class TestBadLimitKey(TestPaginateBase):
196
197
def setUp(self):
198
super(TestBadLimitKey, self).setUp()
199
self.bar_param.type_name = 'bad'
200
201
def test_integer_limit_key(self):
202
argument_table = {
203
'foo': mock.Mock(),
204
'bar': mock.Mock(),
205
}
206
with self.assertRaises(TypeError):
207
paginate.unify_paging_params(argument_table, self.operation_model,
208
'building-argument-table.foo.bar',
209
self.session)
210
211
212
class TestShouldEnablePagination(TestPaginateBase):
213
def setUp(self):
214
super(TestShouldEnablePagination, self).setUp()
215
self.parsed_globals = mock.Mock()
216
self.parsed_args = mock.Mock()
217
self.parsed_args.starting_token = None
218
self.parsed_args.page_size = None
219
self.parsed_args.max_items = None
220
self.call_parameters = {}
221
222
def test_should_not_enable_pagination(self):
223
# Here the user has specified a manual pagination argument,
224
# so we should turn pagination off.
225
# From setUp(), the limit_key is 'Bar'
226
input_tokens = ['foo', 'bar']
227
self.parsed_globals.paginate = True
228
# Corresponds to --bar 10
229
self.parsed_args.foo = None
230
self.parsed_args.bar = 10
231
paginate.check_should_enable_pagination(
232
input_tokens, {}, {}, self.parsed_args, self.parsed_globals)
233
# We should have turned paginate off because the
234
# user specified --bar 10
235
self.assertFalse(self.parsed_globals.paginate)
236
237
def test_should_enable_pagination_with_no_args(self):
238
input_tokens = ['foo', 'bar']
239
self.parsed_globals.paginate = True
240
# Corresponds to not specifying --foo nor --bar
241
self.parsed_args.foo = None
242
self.parsed_args.bar = None
243
paginate.check_should_enable_pagination(
244
input_tokens, {}, {}, self.parsed_args, self.parsed_globals)
245
# We should have turned paginate off because the
246
# user specified --bar 10
247
self.assertTrue(self.parsed_globals.paginate)
248
249
def test_default_to_pagination_on_when_ambiguous(self):
250
input_tokens = ['foo', 'max-items']
251
self.parsed_globals.paginate = True
252
# Here the user specifies --max-items 10 This is ambiguous because the
253
# input_token also contains 'max-items'. Should we assume they want
254
# pagination turned off or should we assume that this is the normalized
255
# --max-items?
256
# Will we default to assuming they meant the normalized
257
# --max-items.
258
self.parsed_args.foo = None
259
self.parsed_args.max_items = 10
260
paginate.check_should_enable_pagination(
261
input_tokens, {}, {}, self.parsed_args, self.parsed_globals)
262
self.assertTrue(self.parsed_globals.paginate,
263
"Pagination was not enabled.")
264
265
def test_fall_back_to_original_max_items_when_pagination_turned_off(self):
266
input_tokens = ['max-items']
267
# User specifies --no-paginate.
268
self.parsed_globals.paginate = False
269
# But also specifies --max-items 10, which is normally a pagination arg
270
# we replace. However, because they've explicitly turned off
271
# pagination, we should put back the original arg.
272
self.parsed_args.max_items = 10
273
shadowed_args = {'max-items': mock.sentinel.ORIGINAL_ARG}
274
arg_table = {'max-items': mock.sentinel.PAGINATION_ARG}
275
276
paginate.check_should_enable_pagination(
277
input_tokens, shadowed_args, arg_table,
278
self.parsed_args, self.parsed_globals)
279
280
def test_shadowed_args_are_replaced_when_pagination_turned_off(self):
281
input_tokens = ['foo', 'bar']
282
self.parsed_globals.paginate = True
283
# Corresponds to --bar 10
284
self.parsed_args.foo = None
285
self.parsed_args.bar = 10
286
shadowed_args = {'foo': mock.sentinel.ORIGINAL_ARG}
287
arg_table = {'foo': mock.sentinel.PAGINATION_ARG}
288
paginate.check_should_enable_pagination(
289
input_tokens, shadowed_args, arg_table,
290
self.parsed_args, self.parsed_globals)
291
# We should have turned paginate off because the
292
# user specified --bar 10
293
self.assertFalse(self.parsed_globals.paginate)
294
self.assertEqual(arg_table['foo'], mock.sentinel.ORIGINAL_ARG)
295
296
def test_shadowed_args_are_replaced_when_pagination_set_off(self):
297
input_tokens = ['foo', 'bar']
298
self.parsed_globals.paginate = False
299
# Corresponds to --bar 10
300
self.parsed_args.foo = None
301
self.parsed_args.bar = 10
302
shadowed_args = {'foo': mock.sentinel.ORIGINAL_ARG}
303
arg_table = {'foo': mock.sentinel.PAGINATION_ARG}
304
paginate.check_should_enable_pagination(
305
input_tokens, shadowed_args, arg_table,
306
self.parsed_args, self.parsed_globals)
307
# We should have turned paginate off because the
308
# user specified --bar 10
309
self.assertFalse(self.parsed_globals.paginate)
310
self.assertEqual(arg_table['foo'], mock.sentinel.ORIGINAL_ARG)
311
312
313
class TestPaginateV2Debug(TestPaginateBase):
314
def setUp(self):
315
super().setUp()
316
self.parsed_globals = mock.Mock()
317
self.parsed_args = mock.Mock()
318
self.parsed_args.starting_token = None
319
self.parsed_args.page_size = None
320
self.parsed_args.max_items = None
321
self.call_parameters = {}
322
323
def _mock_emit_first_non_none_response(
324
self,
325
mock_input_json_data,
326
event_name
327
):
328
if event_name == 'get-cli-input-json-data':
329
return mock_input_json_data
330
return None
331
332
def test_v2_debug_call_parameters(self):
333
# Here the user has specified a manual pagination argument,
334
# via CLI Input JSON and specified v2-debug, so a
335
# migration warning should be printed.
336
# From setUp(), the limit_key is 'Bar'
337
input_tokens = ['Foo', 'Bar']
338
self.parsed_globals.v2_debug = True
339
self.parsed_globals.paginate = True
340
self.session.emit_first_non_none_response.side_effect = partial(
341
self._mock_emit_first_non_none_response,
342
{'Bar': 10}
343
)
344
# Corresponds to --bar 10
345
self.call_parameters['Foo'] = None
346
self.call_parameters['Bar'] = 10
347
with capture_output() as output:
348
paginate.check_should_enable_pagination_call_parameters(
349
self.session,
350
input_tokens,
351
self.call_parameters,
352
{},
353
self.parsed_globals
354
)
355
# We should have printed the migration warning
356
# because the user specified {Bar: 10} in the input JSON
357
self.assertIn(
358
'AWS CLI v2 UPGRADE WARNING: In AWS CLI v2, if you specify '
359
'pagination parameters by using a file with the '
360
'`--cli-input-json` parameter, automatic pagination will be '
361
'turned off.',
362
output.stderr.getvalue()
363
)
364
365
def test_v2_debug_call_params_does_not_print_for_cmd_args(self):
366
# Here the user has specified a pagination argument as a command
367
# argument and specified v2-debug, so the migration warning should NOT
368
# be printed. From setUp(), the limit_key is 'Bar'
369
input_tokens = ['Foo', 'Bar']
370
self.parsed_globals.v2_debug = True
371
self.parsed_globals.paginate = True
372
self.session.emit_first_non_none_response.side_effect = partial(
373
self._mock_emit_first_non_none_response,
374
None
375
)
376
# Corresponds to --bar 10
377
self.call_parameters['Foo'] = None
378
self.call_parameters['Bar'] = 10
379
with capture_output() as output:
380
paginate.check_should_enable_pagination_call_parameters(
381
self.session,
382
input_tokens,
383
self.call_parameters,
384
{},
385
self.parsed_globals
386
)
387
# We should not have printed the warning because
388
# the user did not specify any params through CLI input JSON
389
self.assertNotIn(
390
'AWS CLI v2 UPGRADE WARNING: In AWS CLI v2, if you specify '
391
'pagination parameters by using a file with the '
392
'`--cli-input-json` parameter, automatic pagination will be '
393
'turned off.',
394
output.stderr.getvalue()
395
)
396
397
398
class TestEnsurePagingParamsNotSet(TestPaginateBase):
399
def setUp(self):
400
super(TestEnsurePagingParamsNotSet, self).setUp()
401
self.parsed_args = mock.Mock()
402
403
self.parsed_args.starting_token = None
404
self.parsed_args.page_size = None
405
self.parsed_args.max_items = None
406
407
def test_pagination_params_raise_error_with_no_paginate(self):
408
self.parsed_args.max_items = 100
409
410
with self.assertRaises(PaginationError):
411
paginate.ensure_paging_params_not_set(self.parsed_args, {})
412
413
def test_can_handle_missing_page_size(self):
414
# Not all pagination operations have a page_size.
415
del self.parsed_args.page_size
416
self.assertIsNone(paginate.ensure_paging_params_not_set(
417
self.parsed_args, {}))
418
419
420
class TestNonPositiveMaxItems:
421
def test_positive_integer_does_not_raise_warning(self, max_items_page_arg, capsys):
422
max_items_page_arg.add_to_params({}, 1)
423
captured = capsys.readouterr()
424
assert captured.err == ""
425
426
def test_zero_raises_warning(self, max_items_page_arg, capsys):
427
max_items_page_arg.add_to_params({}, 0)
428
captured = capsys.readouterr()
429
assert "Non-positive values for --max-items" in captured.err
430
431
def test_negative_integer_raises_warning(self, max_items_page_arg, capsys):
432
max_items_page_arg.add_to_params({}, -1)
433
captured = capsys.readouterr()
434
assert "Non-positive values for --max-items" in captured.err
435
436