From 269d682f70d8cf852ce04dda7436dc7b9d65b773 Mon Sep 17 00:00:00 2001 From: Ryan Brown Date: Mon, 30 Apr 2018 16:50:32 -0400 Subject: [PATCH] Move camel2snake and snake2camel to common/dict_transformations.py (#39498) Since it will be used outside just AWS modules, this commit moves `camel_dict_to_snake_dict` and `snake_dict_to_camel_dict` functions into a new module_utils file under common/ to match their wider usage. --- .../common/dict_transformations.py | 107 ++++++++++++++++++ lib/ansible/module_utils/ec2.py | 101 +---------------- 2 files changed, 111 insertions(+), 97 deletions(-) create mode 100644 lib/ansible/module_utils/common/dict_transformations.py 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 00000000000..b02fe0372d9 --- /dev/null +++ b/lib/ansible/module_utils/common/dict_transformations.py @@ -0,0 +1,107 @@ +# -*- 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 + + +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() + + import re + 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() diff --git a/lib/ansible/module_utils/ec2.py b/lib/ansible/module_utils/ec2.py index 33f7f79b03e..7d7f7c2d3a4 100644 --- a/lib/ansible/module_utils/ec2.py +++ b/lib/ansible/module_utils/ec2.py @@ -32,6 +32,10 @@ import re from ansible.module_utils._text import to_native, to_text from ansible.module_utils.cloud import CloudRetry from ansible.module_utils.six import string_types, binary_type, text_type +from ansible.module_utils.common.dict_transformations import ( + camel_dict_to_snake_dict, snake_dict_to_camel_dict, + _camel_to_snake, _snake_to_camel, +) try: import boto @@ -346,103 +350,6 @@ def ec2_connect(module): return ec2 -def _camel_to_snake(name, reversible=False): - - def prepend_underscore_and_lower(m): - return '_' + m.group(0).lower() - - import re - 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 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_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 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 ansible_dict_to_boto3_filter_list(filters_dict): """ Convert an Ansible dict of filters to list of dicts that boto3 can use