diff options
Diffstat (limited to 'lib/ansible/module_utils/common/dict_transformations.py')
-rw-r--r-- | lib/ansible/module_utils/common/dict_transformations.py | 154 |
1 files changed, 154 insertions, 0 deletions
diff --git a/lib/ansible/module_utils/common/dict_transformations.py b/lib/ansible/module_utils/common/dict_transformations.py new file mode 100644 index 0000000..ffd0645 --- /dev/null +++ b/lib/ansible/module_utils/common/dict_transformations.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import re +from copy import deepcopy + +from ansible.module_utils.common._collections_compat import MutableMapping + + +def camel_dict_to_snake_dict(camel_dict, reversible=False, ignore_list=()): + """ + reversible allows two way conversion of a camelized dict + such that snake_dict_to_camel_dict(camel_dict_to_snake_dict(x)) == x + + This is achieved through mapping e.g. HTTPEndpoint to h_t_t_p_endpoint + where the default would be simply http_endpoint, which gets turned into + HttpEndpoint if recamelized. + + ignore_list is used to avoid converting a sub-tree of a dict. This is + particularly important for tags, where keys are case-sensitive. We convert + the 'Tags' key but nothing below. + """ + + def value_is_list(camel_list): + + checked_list = [] + for item in camel_list: + if isinstance(item, dict): + checked_list.append(camel_dict_to_snake_dict(item, reversible)) + elif isinstance(item, list): + checked_list.append(value_is_list(item)) + else: + checked_list.append(item) + + return checked_list + + snake_dict = {} + for k, v in camel_dict.items(): + if isinstance(v, dict) and k not in ignore_list: + snake_dict[_camel_to_snake(k, reversible=reversible)] = camel_dict_to_snake_dict(v, reversible) + elif isinstance(v, list) and k not in ignore_list: + snake_dict[_camel_to_snake(k, reversible=reversible)] = value_is_list(v) + else: + snake_dict[_camel_to_snake(k, reversible=reversible)] = v + + return snake_dict + + +def snake_dict_to_camel_dict(snake_dict, capitalize_first=False): + """ + Perhaps unexpectedly, snake_dict_to_camel_dict returns dromedaryCase + rather than true CamelCase. Passing capitalize_first=True returns + CamelCase. The default remains False as that was the original implementation + """ + + def camelize(complex_type, capitalize_first=False): + if complex_type is None: + return + new_type = type(complex_type)() + if isinstance(complex_type, dict): + for key in complex_type: + new_type[_snake_to_camel(key, capitalize_first)] = camelize(complex_type[key], capitalize_first) + elif isinstance(complex_type, list): + for i in range(len(complex_type)): + new_type.append(camelize(complex_type[i], capitalize_first)) + else: + return complex_type + return new_type + + return camelize(snake_dict, capitalize_first) + + +def _snake_to_camel(snake, capitalize_first=False): + if capitalize_first: + return ''.join(x.capitalize() or '_' for x in snake.split('_')) + else: + return snake.split('_')[0] + ''.join(x.capitalize() or '_' for x in snake.split('_')[1:]) + + +def _camel_to_snake(name, reversible=False): + + def prepend_underscore_and_lower(m): + return '_' + m.group(0).lower() + + if reversible: + upper_pattern = r'[A-Z]' + else: + # Cope with pluralized abbreviations such as TargetGroupARNs + # that would otherwise be rendered target_group_ar_ns + upper_pattern = r'[A-Z]{3,}s$' + + s1 = re.sub(upper_pattern, prepend_underscore_and_lower, name) + # Handle when there was nothing before the plural_pattern + if s1.startswith("_") and not name.startswith("_"): + s1 = s1[1:] + if reversible: + return s1 + + # Remainder of solution seems to be https://stackoverflow.com/a/1176023 + first_cap_pattern = r'(.)([A-Z][a-z]+)' + all_cap_pattern = r'([a-z0-9])([A-Z]+)' + s2 = re.sub(first_cap_pattern, r'\1_\2', s1) + return re.sub(all_cap_pattern, r'\1_\2', s2).lower() + + +def dict_merge(a, b): + '''recursively merges dicts. not just simple a['key'] = b['key'], if + both a and b have a key whose value is a dict then dict_merge is called + on both values and the result stored in the returned dictionary.''' + if not isinstance(b, dict): + return b + result = deepcopy(a) + for k, v in b.items(): + if k in result and isinstance(result[k], dict): + result[k] = dict_merge(result[k], v) + else: + result[k] = deepcopy(v) + return result + + +def recursive_diff(dict1, dict2): + """Recursively diff two dictionaries + + Raises ``TypeError`` for incorrect argument type. + + :arg dict1: Dictionary to compare against. + :arg dict2: Dictionary to compare with ``dict1``. + :return: Tuple of dictionaries of differences or ``None`` if there are no differences. + """ + + if not all((isinstance(item, MutableMapping) for item in (dict1, dict2))): + raise TypeError("Unable to diff 'dict1' %s and 'dict2' %s. " + "Both must be a dictionary." % (type(dict1), type(dict2))) + + left = dict((k, v) for (k, v) in dict1.items() if k not in dict2) + right = dict((k, v) for (k, v) in dict2.items() if k not in dict1) + for k in (set(dict1.keys()) & set(dict2.keys())): + if isinstance(dict1[k], dict) and isinstance(dict2[k], dict): + result = recursive_diff(dict1[k], dict2[k]) + if result: + left[k] = result[0] + right[k] = result[1] + elif dict1[k] != dict2[k]: + left[k] = dict1[k] + right[k] = dict2[k] + if left or right: + return left, right + return None |