Skip to content

Commit

Permalink
Implement reverse JMESPath support to build params
Browse files Browse the repository at this point in the history
This adds proper reverse JMESPath support so that we can now build all
parameters properly. The following did not work before, but does now
because we can build up the filters array structure:

```python
ec2 = boto3.resource('ec2')

instances = ec2.instances.filter(
    Filters=[{'Name': 'instance-type', 'Values': ['m1.small']}])

for instance in instances:
    print(instance.id)
```

Tests are added to cover the various cases of `dict` and `list`
combinations.
  • Loading branch information
danielgtaylor committed Nov 11, 2014
1 parent 334aa9d commit 3df00fd
Show file tree
Hide file tree
Showing 2 changed files with 102 additions and 11 deletions.
73 changes: 63 additions & 10 deletions boto3/resources/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

import re

from botocore import xform_name


INDEX_RE = re.compile('\[(.*)\]$')


def create_request_parameters(parent, request_model):
"""
Handle request parameters that can be filled in from identifiers,
Expand Down Expand Up @@ -45,15 +50,63 @@ def create_request_parameters(parent, request_model):
raise NotImplementedError(
'Unsupported source type: {0}'.format(source_type))

# Basic reverse jmespath support for lists
# TODO: I believe this may get added into jmespath eventually?
# TODO: support foo.bar.baz and foo.bar[0].baz
# jmespath.create_structure(params, target, value)
if target.endswith('[]'):
params[target[:-2]] = [value]
elif target.endswith('[0]'):
params[target[:-3]] = [value]
else:
params[target] = value
build_param_structure(params, target, value)

return params

def build_param_structure(params, target, value):
"""
This method provides a basic reverse JMESPath implementation that
lets you go from a JMESPath-like string to a possibly deeply nested
object. The ``params`` are mutated in-place, so subsequent calls
can modify the same element by its index.
>>> build_param_structure(params, 'test[0]', 1)
>>> print(params)
{'test': [1]}
>>> build_param_structure(params, 'foo.bar[0].baz', 'hello world')
>>> print(params)
{'test': [1], 'foo': {'bar': [{'baz': 'hello, world'}]}}
"""
pos = params
parts = target.split('.')

# First, split into parts like 'foo', 'bar[0]', 'baz' and process
# each piece. It can either be a list or a dict, depending on if
# an index like `[0]` is present. We detect this via a regular
# expression, and keep track of where we are in params via the
# pos variable, walking down to the last item. Once there, we
# set the value.
for i, part in enumerate(parts):
# Is it indexing an array?
result = INDEX_RE.search(part)
if result:
index = int(result.group(1))

# Strip index off part name
part = part[:-len(str(index) + '[]')]

if part not in pos or not isinstance(pos[part], list):
pos[part] = []

while len(pos[part]) <= index:
# Assume it's a dict until we set the final value below
pos[part].append({})

# Last item? Set the value, otherwise set the new position
if i == len(parts) - 1:
pos[part][index] = value
else:
# The new pos is the *item* in the array, not the array!
pos = pos[part][index]
else:
if part not in pos:
pos[part] = {}

# Last item? Set the value, otherwise set the new position
if i == len(parts) - 1:
pos[part] = value
else:
pos = pos[part]
40 changes: 39 additions & 1 deletion tests/unit/resources/test_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
# language governing permissions and limitations under the License.

from boto3.resources.model import Request
from boto3.resources.params import create_request_parameters
from boto3.resources.params import create_request_parameters, \
build_param_structure
from tests import BaseTestCase, mock

class TestServiceActionParams(BaseTestCase):
Expand Down Expand Up @@ -122,3 +123,40 @@ def test_action_params_list(self):
'Parameter list should only have a single item')
self.assertIn('w-url', params['WarehouseUrls'],
'Parameter not in expected list')


class TestStructBuilder(BaseTestCase):
def test_simple_value(self):
params = {}
build_param_structure(params, 'foo', 'bar')
self.assertEqual(params['foo'], 'bar')

def test_nested_dict(self):
params = {}
build_param_structure(params, 'foo.bar.baz', 123)
self.assertEqual(params['foo']['bar']['baz'], 123)

def test_nested_list(self):
params = {}
build_param_structure(params, 'foo.bar[0]', 'test')
self.assertEqual(params['foo']['bar'][0], 'test')

def test_strange_offset(self):
params = {}
build_param_structure(params, 'foo[2]', 'test')
self.assertEqual(params['foo'], [{}, {}, 'test'])

def test_nested_list_dict(self):
params = {}
build_param_structure(params, 'foo.bar[0].baz', 123)
self.assertEqual(params['foo']['bar'][0]['baz'], 123)

def test_modify_existing(self):
params = {
'foo': [
{'key': 'abc'}
]
}
build_param_structure(params, 'foo[0].secret', 123)
self.assertEqual(params['foo'][0]['key'], 'abc')
self.assertEqual(params['foo'][0]['secret'], 123)

0 comments on commit 3df00fd

Please sign in to comment.