Skip to content

Commit 5c85f99

Browse files
authored
Merge pull request #147 from joguSD/coerce-params
Coerce operation params according to model
2 parents 96852f1 + cfab482 commit 5c85f99

File tree

2 files changed

+143
-1
lines changed

2 files changed

+143
-1
lines changed

awsshell/wizard.py

+84
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import six
12
import sys
23
import copy
34
import logging
@@ -18,6 +19,87 @@
1819
LOG = logging.getLogger(__name__)
1920

2021

22+
class ParamCoercion(object):
23+
"""This class coerces string parameters into the correct type.
24+
25+
By default this converts strings to numerical values if the input
26+
parameters model indicates that the field should be a number. This is to
27+
compensate for the fact that values taken in from prompts will always be
28+
strings and avoids having to create specific interactions for simple
29+
conversions or having to specify the type in the wizard specification.
30+
"""
31+
32+
_DEFAULT_DICT = {
33+
'integer': int,
34+
'float': float,
35+
'double': float,
36+
'long': int
37+
}
38+
39+
def __init__(self, type_dict=_DEFAULT_DICT):
40+
"""Initialize a ParamCoercion object.
41+
42+
:type type_dict: dict
43+
:param type_dict: (Optional) A dictionary of converstions. Keys are
44+
strings representing the shape type name and the values are callables
45+
that given a string will return an instance of an appropriate type for
46+
that shape type. Defaults to only coerce numbers.
47+
"""
48+
self._type_dict = type_dict
49+
50+
def coerce(self, params, shape):
51+
"""Coerce the params according to the given shape.
52+
53+
:type params: dict
54+
:param params: The parameters to be given to an operation call.
55+
56+
:type shape: :class:`botocore.model.Shape`
57+
:param shape: The input shape for the desired operation.
58+
59+
:rtype: dict
60+
:return: The coerced version of the params.
61+
"""
62+
name = shape.type_name
63+
if isinstance(params, dict) and name == 'structure':
64+
return self._coerce_structure(params, shape)
65+
elif isinstance(params, dict) and name == 'map':
66+
return self._coerce_map(params, shape)
67+
elif isinstance(params, (list, tuple)) and name == 'list':
68+
return self._coerce_list(params, shape)
69+
elif isinstance(params, six.string_types) and name in self._type_dict:
70+
target_type = self._type_dict[shape.type_name]
71+
return self._coerce_field(params, target_type)
72+
return params
73+
74+
def _coerce_structure(self, params, shape):
75+
members = shape.members
76+
coerced = {}
77+
for param in members:
78+
if param in params:
79+
coerced[param] = self.coerce(params[param], members[param])
80+
return coerced
81+
82+
def _coerce_map(self, params, shape):
83+
coerced = {}
84+
for key, value in params.items():
85+
coerced_key = self.coerce(key, shape.key)
86+
coerced[coerced_key] = self.coerce(value, shape.value)
87+
return coerced
88+
89+
def _coerce_list(self, list_param, shape):
90+
member_shape = shape.member
91+
coerced_list = []
92+
for item in list_param:
93+
coerced_list.append(self.coerce(item, member_shape))
94+
return coerced_list
95+
96+
def _coerce_field(self, value, target_type):
97+
try:
98+
return target_type(value)
99+
except ValueError:
100+
return value
101+
102+
21103
def stage_error_handler(error, stages, confirm=confirm, prompt=select_prompt):
22104
managed_errors = (
23105
ClientError,
@@ -264,6 +346,8 @@ def _handle_request_retrieval(self):
264346
self._env.resolve_parameters(req.get('EnvParameters', {}))
265347
# union of parameters and env_parameters, conflicts favor env params
266348
parameters = dict(parameters, **env_parameters)
349+
model = client.meta.service_model.operation_model(req['Operation'])
350+
parameters = ParamCoercion().coerce(parameters, model.input_shape)
267351
# if the operation supports pagination, load all results upfront
268352
if client.can_paginate(operation_name):
269353
# get paginator and create iterator

tests/unit/test_wizard.py

+59-1
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
import botocore.session
44

55
from botocore.loaders import Loader
6+
from botocore import model
67
from botocore.session import Session
78
from awsshell.utils import FileReadError
8-
from awsshell.wizard import stage_error_handler
9+
from awsshell.wizard import stage_error_handler, ParamCoercion
910
from awsshell.interaction import InteractionException
1011
from botocore.exceptions import ClientError, BotoCoreError
1112
from awsshell.wizard import Environment, WizardLoader, WizardException
@@ -383,3 +384,60 @@ def test_stage_exception_handler_other(error_class):
383384
err = error_class()
384385
res = stage_error_handler(err, ['stage'], confirm=confirm, prompt=prompt)
385386
assert res is None
387+
388+
389+
@pytest.fixture
390+
def test_shape():
391+
shapes = {
392+
"TestShape": {
393+
"type": "structure",
394+
"members": {
395+
"Huge": {"shape": "Long"},
396+
"Map": {"shape": "TestMap"},
397+
"Scale": {"shape": "Double"},
398+
"Count": {"shape": "Integer"},
399+
"Items": {"shape": "TestList"}
400+
}
401+
},
402+
"TestList": {
403+
"type": "list",
404+
"member": {
405+
"shape": "Float"
406+
}
407+
},
408+
"TestMap": {
409+
"type": "map",
410+
"key": {"shape": "Double"},
411+
"value": {"shape": "Integer"}
412+
},
413+
"Long": {"type": "long"},
414+
"Float": {"type": "float"},
415+
"Double": {"type": "double"},
416+
"String": {"type": "string"},
417+
"Integer": {"type": "integer"}
418+
}
419+
return model.ShapeResolver(shapes).get_shape_by_name('TestShape')
420+
421+
422+
def test_param_coercion_numbers(test_shape):
423+
# verify coercion will convert strings to numbers according to shape
424+
params = {
425+
"Count": "5",
426+
"Scale": "2.3",
427+
"Items": ["5", "3.14"],
428+
"Huge": "92233720368547758070",
429+
"Map": {"2": "12"}
430+
}
431+
coerced = ParamCoercion().coerce(params, test_shape)
432+
assert isinstance(coerced['Count'], int)
433+
assert isinstance(coerced['Scale'], float)
434+
assert all(isinstance(item, float) for item in coerced['Items'])
435+
assert coerced['Map'][2] == 12
436+
assert coerced['Huge'] == 92233720368547758070
437+
438+
439+
def test_param_coercion_failure(test_shape):
440+
# verify coercion leaves the field the same when it fails
441+
params = {"Count": "fifty"}
442+
coerced = ParamCoercion().coerce(params, test_shape)
443+
assert coerced["Count"] == params["Count"]

0 commit comments

Comments
 (0)