From 15a58d498db34fb5ccb417609ba2675b221b1e0a Mon Sep 17 00:00:00 2001 From: Will Thames Date: Thu, 14 Dec 2017 06:13:26 +1000 Subject: [PATCH] [cloud] Improve snake <-> camel conversion for AWS utils (#31400) Allow CamelCase version of snake_dict_to_camel_dict (currently only dromedaryCase is supported) Add reversible option to camel_dict_to_snake_dict Add tests for both of these options --- lib/ansible/module_utils/ec2.py | 60 +++++++++++++------ .../module_utils/ec2/test_camel_to_snake.py | 28 ++++++++- 2 files changed, 69 insertions(+), 19 deletions(-) diff --git a/lib/ansible/module_utils/ec2.py b/lib/ansible/module_utils/ec2.py index a8c60c7ee9..e90a17d01b 100644 --- a/lib/ansible/module_utils/ec2.py +++ b/lib/ansible/module_utils/ec2.py @@ -343,19 +343,26 @@ def ec2_connect(module): return ec2 -def _camel_to_snake(name): +def _camel_to_snake(name, reversible=False): def prepend_underscore_and_lower(m): return '_' + m.group(0).lower() import re - # Cope with pluralized abbreviations such as TargetGroupARNs - # that would otherwise be rendered target_group_ar_ns - plural_pattern = r'[A-Z]{3,}s$' - s1 = re.sub(plural_pattern, prepend_underscore_and_lower, name) + 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]+)' @@ -363,14 +370,22 @@ def _camel_to_snake(name): return re.sub(all_cap_pattern, r'\1_\2', s2).lower() -def camel_dict_to_snake_dict(camel_dict): +def camel_dict_to_snake_dict(camel_dict, reversible=False): + """ + 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. + """ 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)) + checked_list.append(camel_dict_to_snake_dict(item, reversible)) elif isinstance(item, list): checked_list.append(value_is_list(item)) else: @@ -381,35 +396,44 @@ def camel_dict_to_snake_dict(camel_dict): snake_dict = {} for k, v in camel_dict.items(): if isinstance(v, dict): - snake_dict[_camel_to_snake(k)] = camel_dict_to_snake_dict(v) + snake_dict[_camel_to_snake(k, reversible=reversible)] = camel_dict_to_snake_dict(v, reversible) elif isinstance(v, list): - snake_dict[_camel_to_snake(k)] = value_is_list(v) + snake_dict[_camel_to_snake(k, reversible=reversible)] = value_is_list(v) else: - snake_dict[_camel_to_snake(k)] = v + snake_dict[_camel_to_snake(k, reversible=reversible)] = v return snake_dict -def snake_dict_to_camel_dict(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 camelize(complex_type): + +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[camel(key)] = camelize(complex_type[key]) + 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])) + new_type.append(camelize(complex_type[i], capitalize_first)) else: return complex_type return new_type - def camel(words): - return words.split('_')[0] + ''.join(x.capitalize() or '_' for x in words.split('_')[1:]) - - return camelize(snake_dict) + return camelize(snake_dict, capitalize_first) def ansible_dict_to_boto3_filter_list(filters_dict): diff --git a/test/units/module_utils/ec2/test_camel_to_snake.py b/test/units/module_utils/ec2/test_camel_to_snake.py index 0bb5d6829e..438e02ba15 100644 --- a/test/units/module_utils/ec2/test_camel_to_snake.py +++ b/test/units/module_utils/ec2/test_camel_to_snake.py @@ -17,7 +17,7 @@ # along with Ansible. If not, see . from ansible.compat.tests import unittest -from ansible.module_utils.ec2 import _camel_to_snake +from ansible.module_utils.ec2 import _camel_to_snake, _snake_to_camel EXPECTED_SNAKIFICATION = { 'alllower': 'alllower', @@ -29,9 +29,35 @@ EXPECTED_SNAKIFICATION = { 'PLURALs': 'plurals' } +EXPECTED_REVERSIBLE = { + 'TwoWords': 'two_words', + 'AllUpperAtEND': 'all_upper_at_e_n_d', + 'AllUpperButPLURALs': 'all_upper_but_p_l_u_r_a_ls', + 'TargetGroupARNs': 'target_group_a_r_ns', + 'HTTPEndpoints': 'h_t_t_p_endpoints', + 'PLURALs': 'p_l_u_r_a_ls' +} + class CamelToSnakeTestCase(unittest.TestCase): def test_camel_to_snake(self): for (k, v) in EXPECTED_SNAKIFICATION.items(): self.assertEqual(_camel_to_snake(k), v) + + def test_reversible_camel_to_snake(self): + for (k, v) in EXPECTED_REVERSIBLE.items(): + self.assertEqual(_camel_to_snake(k, reversible=True), v) + + +class SnakeToCamelTestCase(unittest.TestCase): + + def test_snake_to_camel_reversed(self): + for (k, v) in EXPECTED_REVERSIBLE.items(): + self.assertEqual(_snake_to_camel(v, capitalize_first=True), k) + + +class CamelToSnakeAndBackTestCase(unittest.TestCase): + def test_camel_to_snake_and_back(self): + for (k, v) in EXPECTED_REVERSIBLE.items(): + self.assertEqual(_snake_to_camel(_camel_to_snake(k, reversible=True), capitalize_first=True), k)