Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 44 additions & 5 deletions samcli/yamlhelper.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
# Copyright 2012-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
"""
Helper to be able to parse/dump YAML files
YAML helper, sourced from the AWS CLI

https://github.com/aws/aws-cli/blob/develop/awscli/customizations/cloudformation/yamlhelper.py
"""
# pylint: disable=too-many-ancestors

import json
import six
from botocore.compat import OrderedDict

import yaml
from yaml.resolver import ScalarNode, SequenceNode

import six


def intrinsics_multi_constructor(loader, tag_prefix, node):
"""
Expand Down Expand Up @@ -46,13 +63,28 @@ def intrinsics_multi_constructor(loader, tag_prefix, node):
return {cfntag: value}


def _dict_representer(dumper, data):
return dumper.represent_dict(data.items())


def yaml_dump(dict_to_dump):
"""
Dumps the dictionary as a YAML document
:param dict_to_dump:
:return:
"""
return yaml.safe_dump(dict_to_dump, default_flow_style=False)
FlattenAliasDumper.add_representer(OrderedDict, _dict_representer)
return yaml.dump(
dict_to_dump,
default_flow_style=False,
Dumper=FlattenAliasDumper,
)


def _dict_constructor(loader, node):
# Necessary in order to make yaml merge tags work
loader.flatten_mapping(node)
return OrderedDict(loader.construct_pairs(node))


def yaml_parse(yamlstr):
Expand All @@ -61,7 +93,14 @@ def yaml_parse(yamlstr):
# PyYAML doesn't support json as well as it should, so if the input
# is actually just json it is better to parse it with the standard
# json parser.
return json.loads(yamlstr)
return json.loads(yamlstr, object_pairs_hook=OrderedDict)
except ValueError:
yaml.SafeLoader.add_multi_constructor("!", intrinsics_multi_constructor)
yaml.SafeLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _dict_constructor)
yaml.SafeLoader.add_multi_constructor(
"!", intrinsics_multi_constructor)
return yaml.safe_load(yamlstr)


class FlattenAliasDumper(yaml.SafeDumper):
def ignore_aliases(self, data):
return True
120 changes: 110 additions & 10 deletions tests/unit/test_yamlhelper.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
"""
Helper to be able to parse/dump YAML files
"""
# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0e
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
from botocore.compat import OrderedDict

from unittest import TestCase
from samcli.yamlhelper import yaml_parse, yaml_dump
Expand Down Expand Up @@ -48,17 +58,18 @@ class TestYaml(TestCase):

def test_yaml_with_tags(self):
output = yaml_parse(self.yaml_with_tags)
self.assertEquals(self.parsed_yaml_dict, output)
self.assertEqual(self.parsed_yaml_dict, output)

# Make sure formatter and parser work well with each other
formatted_str = yaml_dump(output)
output_again = yaml_parse(formatted_str)
self.assertEquals(output, output_again)
self.assertEqual(output, output_again)

def test_yaml_getatt(self):
# This is an invalid syntax for !GetAtt. But make sure the code does not crash when we encouter this syntax
# Let CloudFormation interpret this value at runtime
input = """
# This is an invalid syntax for !GetAtt. But make sure the code does
# not crash when we encounter this syntax. Let CloudFormation
# interpret this value at runtime
yaml_input = """
Resource:
Key: !GetAtt ["a", "b"]
"""
Expand All @@ -68,13 +79,102 @@ def test_yaml_getatt(self):
"Key": {
"Fn::GetAtt": ["a", "b"]
}

}
}

actual_output = yaml_parse(input)
self.assertEquals(actual_output, output)
actual_output = yaml_parse(yaml_input)
self.assertEqual(actual_output, output)

def test_parse_json_with_tabs(self):
template = '{\n\t"foo": "bar"\n}'
output = yaml_parse(template)
self.assertEqual(output, {'foo': 'bar'})

def test_parse_json_preserve_elements_order(self):
input_template = """
{
"B_Resource": {
"Key2": {
"Name": "name2"
},
"Key1": {
"Name": "name1"
}
},
"A_Resource": {
"Key2": {
"Name": "name2"
},
"Key1": {
"Name": "name1"
}
}
}
"""
expected_dict = OrderedDict([
('B_Resource', OrderedDict([('Key2', {'Name': 'name2'}), ('Key1', {'Name': 'name1'})])),
('A_Resource', OrderedDict([('Key2', {'Name': 'name2'}), ('Key1', {'Name': 'name1'})]))
])
output_dict = yaml_parse(input_template)
self.assertEqual(expected_dict, output_dict)

def test_parse_yaml_preserve_elements_order(self):
input_template = (
'B_Resource:\n'
' Key2:\n'
' Name: name2\n'
' Key1:\n'
' Name: name1\n'
'A_Resource:\n'
' Key2:\n'
' Name: name2\n'
' Key1:\n'
' Name: name1\n'
)
output_dict = yaml_parse(input_template)
expected_dict = OrderedDict([
('B_Resource', OrderedDict([('Key2', {'Name': 'name2'}), ('Key1', {'Name': 'name1'})])),
('A_Resource', OrderedDict([('Key2', {'Name': 'name2'}), ('Key1', {'Name': 'name1'})]))
])
self.assertEqual(expected_dict, output_dict)

output_template = yaml_dump(output_dict)
self.assertEqual(input_template, output_template)

def test_yaml_merge_tag(self):
test_yaml = """
base: &base
property: value
test:
<<: *base
"""
output = yaml_parse(test_yaml)
self.assertTrue(isinstance(output, OrderedDict))
self.assertEqual(output.get('test').get('property'), 'value')

def test_unroll_yaml_anchors(self):
properties = {
"Foo": "bar",
"Spam": "eggs",
}
template = {
"Resources": {
"Resource1": {"Properties": properties},
"Resource2": {"Properties": properties}
}
}

expected = (
'Resources:\n'
' Resource1:\n'
' Properties:\n'
' Foo: bar\n'
' Spam: eggs\n'
' Resource2:\n'
' Properties:\n'
' Foo: bar\n'
' Spam: eggs\n'
)
actual = yaml_dump(template)
self.assertEqual(actual, expected)