From 778931f28f9c02123f407933e81e6405944f61e7 Mon Sep 17 00:00:00 2001 From: Kamil Kozik Date: Fri, 18 Apr 2025 12:51:42 +0200 Subject: [PATCH] preserve literals of e-notation floats in transformation to JSON; support reconstruction of e-notation floats --- CHANGELOG.md | 1 + hcl2/reconstructor.py | 39 ++++++++++++++++--- hcl2/transformer.py | 37 ++++++++++++------ .../terraform-config-json/test_floats.json | 30 ++++++++++++++ test/helpers/terraform-config/test_floats.tf | 20 ++++++++++ test/unit/test_hcl2_syntax.py | 12 +++--- 6 files changed, 117 insertions(+), 22 deletions(-) create mode 100644 test/helpers/terraform-config-json/test_floats.json create mode 100644 test/helpers/terraform-config/test_floats.tf diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ceffae1..a9ec5b7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Issue parsing ellipsis in a separate line within `for` expression ([#221](https://github.com/amplify-education/python-hcl2/pull/221)) - Issue parsing inline expression as an object key; **see Limitations in README.md** ([#222](https://github.com/amplify-education/python-hcl2/pull/222)) +- Preserve literals of e-notation floats in parsing and reconstruction. Thanks, @eranor ([#226](https://github.com/amplify-education/python-hcl2/pull/226)) ## \[7.1.0\] - 2025-04-10 diff --git a/hcl2/reconstructor.py b/hcl2/reconstructor.py index bf653c05..e0cd76bf 100644 --- a/hcl2/reconstructor.py +++ b/hcl2/reconstructor.py @@ -260,6 +260,7 @@ def _should_add_space(self, rule, current_terminal): Terminal("STRING_LIT"), Terminal("DECIMAL"), Terminal("NAME"), + Terminal("NEGATIVE_DECIMAL"), ]: return True @@ -412,10 +413,7 @@ def _newline(self, level: int, count: int = 1) -> Tree: def _is_block(self, value: Any) -> bool: if isinstance(value, dict): block_body = value - if ( - START_LINE_KEY in block_body.keys() - or END_LINE_KEY in block_body.keys() - ): + if START_LINE_KEY in block_body.keys() or END_LINE_KEY in block_body.keys(): return True try: @@ -520,7 +518,7 @@ def _transform_dict_to_body(self, hcl_dict: dict, level: int) -> Tree: return Tree(Token("RULE", "body"), children) - # pylint: disable=too-many-branches, too-many-return-statements + # pylint: disable=too-many-branches, too-many-return-statements too-many-statements def _transform_value_to_expr_term(self, value, level) -> Union[Token, Tree]: """Transforms a value from a dictionary into an "expr_term" (a value in HCL2) @@ -611,6 +609,37 @@ def _transform_value_to_expr_term(self, value, level) -> Union[Token, Tree]: ], ) + if isinstance(value, float): + value = str(value) + literal = [] + + if value[0] == "-": + # pop two first chars - minus and a digit + literal.append(Token("NEGATIVE_DECIMAL", value[:2])) + value = value[2:] + + while value != "": + char = value[0] + + if char == ".": + # current char marks beginning of decimal part: pop all remaining chars and end the loop + literal.append(Token("DOT", char)) + literal.extend(Token("DECIMAL", char) for char in value[1:]) + break + + if char == "e": + # current char marks beginning of e-notation: pop all remaining chars and end the loop + literal.append(Token("EXP_MARK", value)) + break + + literal.append(Token("DECIMAL", char)) + value = value[1:] + + return Tree( + Token("RULE", "expr_term"), + [Tree(Token("RULE", "float_lit"), literal)], + ) + # store strings as single literals if isinstance(value, str): # potentially unpack a complex syntax structure diff --git a/hcl2/transformer.py b/hcl2/transformer.py index 7c7e4bd8..670eb617 100644 --- a/hcl2/transformer.py +++ b/hcl2/transformer.py @@ -44,7 +44,10 @@ def __init__(self, with_meta: bool = False): super().__init__() def float_lit(self, args: List) -> float: - return float("".join([self.to_tf_inline(arg) for arg in args])) + value = "".join([self.to_tf_inline(arg) for arg in args]) + if "e" in value: + return self.to_string_dollar(value) + return float(value) def int_lit(self, args: List) -> int: return int("".join([self.to_tf_inline(arg) for arg in args])) @@ -177,7 +180,9 @@ def conditional(self, args: List) -> str: return f"{args[0]} ? {args[1]} : {args[2]}" def binary_op(self, args: List) -> str: - return " ".join([self.to_tf_inline(arg) for arg in args]) + return " ".join( + [self.unwrap_string_dollar(self.to_tf_inline(arg)) for arg in args] + ) def unary_op(self, args: List) -> str: args = self.process_nulls(args) @@ -304,21 +309,31 @@ def strip_new_line_tokens(self, args: List) -> List: """ return [arg for arg in args if arg != "\n" and arg is not Discard] + def is_string_dollar(self, value: str) -> bool: + if not isinstance(value, str): + return False + return value.startswith("${") and value.endswith("}") + def to_string_dollar(self, value: Any) -> Any: """Wrap a string in ${ and }""" - if isinstance(value, str): + if not isinstance(value, str): + return value # if it's already wrapped, pass it unmodified - if value.startswith("${") and value.endswith("}"): - return value + if self.is_string_dollar(value): + return value - if value.startswith('"') and value.endswith('"'): - value = str(value)[1:-1] - return self.process_escape_sequences(value) + if value.startswith('"') and value.endswith('"'): + value = str(value)[1:-1] + return self.process_escape_sequences(value) + + if self.is_type_keyword(value): + return value - if self.is_type_keyword(value): - return value + return f"${{{value}}}" - return f"${{{value}}}" + def unwrap_string_dollar(self, value: str): + if self.is_string_dollar(value): + return value[2:-1] return value def strip_quotes(self, value: Any) -> Any: diff --git a/test/helpers/terraform-config-json/test_floats.json b/test/helpers/terraform-config-json/test_floats.json new file mode 100644 index 00000000..87ed65c3 --- /dev/null +++ b/test/helpers/terraform-config-json/test_floats.json @@ -0,0 +1,30 @@ +{ + "locals": [ + { + "simple_float": 123.456, + "small_float": 0.123, + "large_float": 9876543.21, + "negative_float": -42.5, + "negative_small": -0.001, + "scientific_positive": "${1.23e5}", + "scientific_negative": "${9.87e-3}", + "scientific_large": "${6.022e+23}", + "integer_as_float": 100.0, + "float_calculation": "${105e+2 * 3.0 / 2.1}", + "float_comparison": "${5e1 > 2.3 ? 1.0 : 0.0}", + "float_list": [ + 1.1, + 2.2, + 3.3, + -4.4, + "${5.5e2}" + ], + "float_object": { + "pi": 3.14159, + "euler": 2.71828, + "sqrt2": 1.41421, + "scientific": "${-123e+2}" + } + } + ] +} diff --git a/test/helpers/terraform-config/test_floats.tf b/test/helpers/terraform-config/test_floats.tf new file mode 100644 index 00000000..90693d41 --- /dev/null +++ b/test/helpers/terraform-config/test_floats.tf @@ -0,0 +1,20 @@ +locals { + simple_float = 123.456 + small_float = 0.123 + large_float = 9876543.21 + negative_float = -42.5 + negative_small = -0.001 + scientific_positive = 1.23e5 + scientific_negative = 9.87e-3 + scientific_large = 6.022e+23 + integer_as_float = 100.0 + float_calculation = 105e+2 * 3.0 / 2.1 + float_comparison = 5e1 > 2.3 ? 1.0 : 0.0 + float_list = [1.1, 2.2, 3.3, -4.4, 5.5e2] + float_object = { + pi = 3.14159 + euler = 2.71828 + sqrt2 = 1.41421 + scientific = -123e+2 + } +} diff --git a/test/unit/test_hcl2_syntax.py b/test/unit/test_hcl2_syntax.py index 440d1b68..96113df3 100644 --- a/test/unit/test_hcl2_syntax.py +++ b/test/unit/test_hcl2_syntax.py @@ -156,12 +156,12 @@ def test_index(self): def test_e_notation(self): literals = { - "var = 3e4": {"var": 30000.0}, - "var = 3.5e5": {"var": 350000.0}, - "var = -3e6": {"var": -3e6}, - "var = -2.3e4": {"var": -2.3e4}, - "var = -5e-2": {"var": -5e-2}, - "var = -6.1e-3": {"var": -6.1e-3}, + "var = 3e4": {"var": "${3e4}"}, + "var = 3.5e5": {"var": "${3.5e5}"}, + "var = -3e6": {"var": "${-3e6}"}, + "var = -2.3e4": {"var": "${-2.3e4}"}, + "var = -5e-2": {"var": "${-5e-2}"}, + "var = -6.1e-3": {"var": "${-6.1e-3}"}, } for actual, expected in literals.items(): result = self.load_to_dict(actual)