Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aws
GitHub Repository: aws/aws-cli
Path: blob/develop/awscli/arguments.py
1566 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
"""Abstractions for CLI arguments.
14
15
This module contains abstractions for representing CLI arguments.
16
This includes how the CLI argument parser is created, how arguments
17
are serialized, and how arguments are bound (if at all) to operation
18
arguments.
19
20
The BaseCLIArgument is the interface for all arguments. This is the interface
21
expected by objects that work with arguments. If you want to implement your
22
own argument subclass, make sure it implements everything in BaseCLIArgument.
23
24
Arguments generally fall into one of several categories:
25
26
* global argument. These arguments may influence what the CLI does,
27
but aren't part of the input parameters needed to make an API call. For
28
example, the ``--region`` argument specifies which region to send the request
29
to. The ``--output`` argument specifies how to display the response to the
30
user. The ``--query`` argument specifies how to select specific elements
31
from a response.
32
* operation argument. These are arguments that influence the parameters we
33
send to a service when making an API call. Some of these arguments are
34
automatically created directly from introspecting the JSON service model.
35
Sometimes customizations may provide a pseudo-argument that takes the
36
user input and maps the input value to several API parameters.
37
38
"""
39
40
import logging
41
42
from botocore.hooks import first_non_none_response
43
44
from awscli.argprocess import unpack_cli_arg
45
from awscli.schema import SchemaTransformer
46
from botocore import model, xform_name
47
48
LOG = logging.getLogger('awscli.arguments')
49
50
51
class UnknownArgumentError(Exception):
52
pass
53
54
55
def create_argument_model_from_schema(schema):
56
# Given a JSON schema (described in schema.py), convert it
57
# to a shape object from `botocore.model.Shape` that can be
58
# used as the argument_model for the Argument classes below.
59
transformer = SchemaTransformer()
60
shapes_map = transformer.transform(schema)
61
shape_resolver = model.ShapeResolver(shapes_map)
62
# The SchemaTransformer guarantees that the top level shape
63
# will always be named 'InputShape'.
64
arg_shape = shape_resolver.get_shape_by_name('InputShape')
65
return arg_shape
66
67
68
class BaseCLIArgument:
69
"""Interface for CLI argument.
70
71
This class represents the interface used for representing CLI
72
arguments.
73
74
"""
75
76
def __init__(self, name):
77
self._name = name
78
79
def add_to_arg_table(self, argument_table):
80
"""Add this object to the argument_table.
81
82
The ``argument_table`` represents the argument for the operation.
83
This is called by the ``ServiceOperation`` object to create the
84
arguments associated with the operation.
85
86
:type argument_table: dict
87
:param argument_table: The argument table. The key is the argument
88
name, and the value is an object implementing this interface.
89
"""
90
argument_table[self.name] = self
91
92
def add_to_parser(self, parser):
93
"""Add this object to the parser instance.
94
95
This method is called by the associated ``ArgumentParser``
96
instance. This method should make the relevant calls
97
to ``add_argument`` to add itself to the argparser.
98
99
:type parser: ``argparse.ArgumentParser``.
100
:param parser: The argument parser associated with the operation.
101
102
"""
103
pass
104
105
def add_to_params(self, parameters, value):
106
"""Add this object to the parameters dict.
107
108
This method is responsible for taking the value specified
109
on the command line, and deciding how that corresponds to
110
parameters used by the service/operation.
111
112
:type parameters: dict
113
:param parameters: The parameters dictionary that will be
114
given to ``botocore``. This should match up to the
115
parameters associated with the particular operation.
116
117
:param value: The value associated with the CLI option.
118
119
"""
120
pass
121
122
@property
123
def name(self):
124
return self._name
125
126
@property
127
def cli_name(self):
128
return '--' + self._name
129
130
@property
131
def cli_type_name(self):
132
raise NotImplementedError("cli_type_name")
133
134
@property
135
def required(self):
136
raise NotImplementedError("required")
137
138
@property
139
def documentation(self):
140
raise NotImplementedError("documentation")
141
142
@property
143
def cli_type(self):
144
raise NotImplementedError("cli_type")
145
146
@property
147
def py_name(self):
148
return self._name.replace('-', '_')
149
150
@property
151
def choices(self):
152
"""List valid choices for argument value.
153
154
If this value is not None then this should return a list of valid
155
values for the argument.
156
157
"""
158
return None
159
160
@property
161
def synopsis(self):
162
return ''
163
164
@property
165
def positional_arg(self):
166
return False
167
168
@property
169
def nargs(self):
170
return None
171
172
@name.setter
173
def name(self, value):
174
self._name = value
175
176
@property
177
def group_name(self):
178
"""Get the group name associated with the argument.
179
180
An argument can be part of a group. This property will
181
return the name of that group.
182
183
This base class has no default behavior for groups, code
184
that consumes argument objects can use them for whatever
185
purposes they like (documentation, mutually exclusive group
186
validation, etc.).
187
188
"""
189
return None
190
191
192
class CustomArgument(BaseCLIArgument):
193
"""
194
Represents a CLI argument that is configured from a dictionary.
195
196
For example, the "top level" arguments used for the CLI
197
(--region, --output) can use a CustomArgument argument,
198
as these are described in the cli.json file as dictionaries.
199
200
This class is also useful for plugins/customizations that want to
201
add additional args.
202
203
"""
204
205
def __init__(
206
self,
207
name,
208
help_text='',
209
dest=None,
210
default=None,
211
action=None,
212
required=None,
213
choices=None,
214
nargs=None,
215
cli_type_name=None,
216
group_name=None,
217
positional_arg=False,
218
no_paramfile=False,
219
argument_model=None,
220
synopsis='',
221
const=None,
222
):
223
self._name = name
224
self._help = help_text
225
self._dest = dest
226
self._default = default
227
self._action = action
228
self._required = required
229
self._nargs = nargs
230
self._const = const
231
self._cli_type_name = cli_type_name
232
self._group_name = group_name
233
self._positional_arg = positional_arg
234
if choices is None:
235
choices = []
236
self._choices = choices
237
self._synopsis = synopsis
238
239
# These are public attributes that are ok to access from external
240
# objects.
241
self.no_paramfile = no_paramfile
242
self.argument_model = None
243
244
if argument_model is None:
245
argument_model = self._create_scalar_argument_model()
246
self.argument_model = argument_model
247
248
# If the top level element is a list then set nargs to
249
# accept multiple values separated by a space.
250
if (
251
self.argument_model is not None
252
and self.argument_model.type_name == 'list'
253
):
254
self._nargs = '+'
255
256
def _create_scalar_argument_model(self):
257
if self._nargs is not None:
258
# If nargs is not None then argparse will parse the value
259
# as a list, so we don't create an argument_object so we don't
260
# go through param validation.
261
return None
262
# If no argument model is provided, we create a basic
263
# shape argument.
264
type_name = self.cli_type_name
265
return create_argument_model_from_schema({'type': type_name})
266
267
@property
268
def cli_name(self):
269
if self._positional_arg:
270
return self._name
271
else:
272
return '--' + self._name
273
274
def add_to_parser(self, parser):
275
"""
276
277
See the ``BaseCLIArgument.add_to_parser`` docs for more information.
278
279
"""
280
cli_name = self.cli_name
281
kwargs = {}
282
if self._dest is not None:
283
kwargs['dest'] = self._dest
284
if self._action is not None:
285
kwargs['action'] = self._action
286
if self._default is not None:
287
kwargs['default'] = self._default
288
if self._choices:
289
kwargs['choices'] = self._choices
290
if self._required is not None:
291
kwargs['required'] = self._required
292
if self._nargs is not None:
293
kwargs['nargs'] = self._nargs
294
if self._const is not None:
295
kwargs['const'] = self._const
296
parser.add_argument(cli_name, **kwargs)
297
298
@property
299
def required(self):
300
if self._required is None:
301
return False
302
return self._required
303
304
@required.setter
305
def required(self, value):
306
self._required = value
307
308
@property
309
def documentation(self):
310
return self._help
311
312
@property
313
def cli_type_name(self):
314
if self._cli_type_name is not None:
315
return self._cli_type_name
316
elif self._action in ['store_true', 'store_false']:
317
return 'boolean'
318
elif self.argument_model is not None:
319
return self.argument_model.type_name
320
else:
321
# Default to 'string' type if we don't have any
322
# other info.
323
return 'string'
324
325
@property
326
def cli_type(self):
327
cli_type = str
328
if self._action in ['store_true', 'store_false']:
329
cli_type = bool
330
return cli_type
331
332
@property
333
def choices(self):
334
return self._choices
335
336
@property
337
def group_name(self):
338
return self._group_name
339
340
@property
341
def synopsis(self):
342
return self._synopsis
343
344
@property
345
def positional_arg(self):
346
return self._positional_arg
347
348
@property
349
def nargs(self):
350
return self._nargs
351
352
353
class CLIArgument(BaseCLIArgument):
354
"""Represents a CLI argument that maps to a service parameter."""
355
356
TYPE_MAP = {
357
'structure': str,
358
'map': str,
359
'timestamp': str,
360
'list': str,
361
'string': str,
362
'float': float,
363
'integer': str,
364
'long': int,
365
'boolean': bool,
366
'double': float,
367
'blob': str,
368
}
369
370
def __init__(
371
self,
372
name,
373
argument_model,
374
operation_model,
375
event_emitter,
376
is_required=False,
377
serialized_name=None,
378
):
379
"""
380
381
:type name: str
382
:param name: The name of the argument in "cli" form
383
(e.g. ``min-instances``).
384
385
:type argument_model: ``botocore.model.Shape``
386
:param argument_model: The shape object that models the argument.
387
388
:type argument_model: ``botocore.model.OperationModel``
389
:param argument_model: The object that models the associated operation.
390
391
:type event_emitter: ``botocore.hooks.BaseEventHooks``
392
:param event_emitter: The event emitter to use when emitting events.
393
This class will emit events during parts of the argument
394
parsing process. This event emitter is what is used to emit
395
such events.
396
397
:type is_required: boolean
398
:param is_required: Indicates if this parameter is required or not.
399
400
"""
401
self._name = name
402
# This is the name we need to use when constructing the parameters
403
# dict we send to botocore. While we can change the .name attribute
404
# which is the name exposed in the CLI, the serialized name we use
405
# for botocore is invariant and should not be changed.
406
if serialized_name is None:
407
serialized_name = name
408
self._serialized_name = serialized_name
409
self.argument_model = argument_model
410
self._required = is_required
411
self._operation_model = operation_model
412
self._event_emitter = event_emitter
413
self._documentation = argument_model.documentation
414
415
@property
416
def py_name(self):
417
return self._name.replace('-', '_')
418
419
@property
420
def required(self):
421
return self._required
422
423
@required.setter
424
def required(self, value):
425
self._required = value
426
427
@property
428
def documentation(self):
429
return self._documentation
430
431
@documentation.setter
432
def documentation(self, value):
433
self._documentation = value
434
435
@property
436
def cli_type_name(self):
437
return self.argument_model.type_name
438
439
@property
440
def cli_type(self):
441
return self.TYPE_MAP.get(self.argument_model.type_name, str)
442
443
def add_to_parser(self, parser):
444
"""
445
446
See the ``BaseCLIArgument.add_to_parser`` docs for more information.
447
448
"""
449
cli_name = self.cli_name
450
parser.add_argument(
451
cli_name,
452
help=self.documentation,
453
type=self.cli_type,
454
required=self.required,
455
)
456
457
def add_to_params(self, parameters, value):
458
if value is None:
459
return
460
else:
461
# This is a two step process. First is the process of converting
462
# the command line value into a python value. Normally this is
463
# handled by argparse directly, but there are cases where extra
464
# processing is needed. For example, "--foo name=value" the value
465
# can be converted from "name=value" to {"name": "value"}. This is
466
# referred to as the "unpacking" process. Once we've unpacked the
467
# argument value, we have to decide how this is converted into
468
# something that can be consumed by botocore. Many times this is
469
# just associating the key and value in the params dict as down
470
# below. Sometimes this can be more complicated, and subclasses
471
# can customize as they need.
472
unpacked = self._unpack_argument(value)
473
LOG.debug(
474
'Unpacked value of %r for parameter "%s": %r',
475
value,
476
self.py_name,
477
unpacked,
478
)
479
parameters[self._serialized_name] = unpacked
480
481
def _unpack_argument(self, value):
482
service_name = self._operation_model.service_model.service_name
483
operation_name = xform_name(self._operation_model.name, '-')
484
override = self._emit_first_response(
485
f'process-cli-arg.{service_name}.{operation_name}',
486
param=self.argument_model,
487
cli_argument=self,
488
value=value,
489
)
490
if override is not None:
491
# A plugin supplied an alternate conversion,
492
# use it instead.
493
return override
494
else:
495
# Fall back to the default arg processing.
496
return unpack_cli_arg(self, value)
497
498
def _emit(self, name, **kwargs):
499
return self._event_emitter.emit(name, **kwargs)
500
501
def _emit_first_response(self, name, **kwargs):
502
responses = self._emit(name, **kwargs)
503
return first_non_none_response(responses)
504
505
506
class ListArgument(CLIArgument):
507
def add_to_parser(self, parser):
508
cli_name = self.cli_name
509
parser.add_argument(
510
cli_name, nargs='*', type=self.cli_type, required=self.required
511
)
512
513
514
class BooleanArgument(CLIArgument):
515
"""Represent a boolean CLI argument.
516
517
A boolean parameter is specified without a value::
518
519
aws foo bar --enabled
520
521
For cases where the boolean parameter is required we need to add
522
two parameters::
523
524
aws foo bar --enabled
525
aws foo bar --no-enabled
526
527
We use the capabilities of the CLIArgument to help achieve this.
528
529
"""
530
531
def __init__(
532
self,
533
name,
534
argument_model,
535
operation_model,
536
event_emitter,
537
is_required=False,
538
action='store_true',
539
dest=None,
540
group_name=None,
541
default=None,
542
serialized_name=None,
543
):
544
super().__init__(
545
name,
546
argument_model,
547
operation_model,
548
event_emitter,
549
is_required,
550
serialized_name=serialized_name,
551
)
552
self._mutex_group = None
553
self._action = action
554
if dest is None:
555
self._destination = self.py_name
556
else:
557
self._destination = dest
558
if group_name is None:
559
self._group_name = self.name
560
else:
561
self._group_name = group_name
562
self._default = default
563
564
def add_to_params(self, parameters, value):
565
# If a value was explicitly specified (so value is True/False
566
# but *not* None) then we add it to the params dict.
567
# If the value was not explicitly set (value is None)
568
# we don't add it to the params dict.
569
if value is not None:
570
parameters[self._serialized_name] = value
571
572
def add_to_arg_table(self, argument_table):
573
# Boolean parameters are a bit tricky. For a single boolean parameter
574
# we actually want two CLI params, a --foo, and a --no-foo. To do this
575
# we need to add two entries to the argument table. So we can add
576
# ourself as the positive option (--no), and then create a clone of
577
# ourselves for the negative service. We then insert both into the
578
# arg table.
579
argument_table[self.name] = self
580
negative_name = 'no-%s' % self.name
581
negative_version = self.__class__(
582
negative_name,
583
self.argument_model,
584
self._operation_model,
585
self._event_emitter,
586
action='store_false',
587
dest=self._destination,
588
group_name=self.group_name,
589
serialized_name=self._serialized_name,
590
)
591
argument_table[negative_name] = negative_version
592
593
def add_to_parser(self, parser):
594
parser.add_argument(
595
self.cli_name,
596
help=self.documentation,
597
action=self._action,
598
default=self._default,
599
dest=self._destination,
600
)
601
602
@property
603
def group_name(self):
604
return self._group_name
605
606