python-hcl2 7.0.1__tar.gz → 7.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/CHANGELOG.md +27 -0
  2. {python_hcl2-7.0.1/python_hcl2.egg-info → python_hcl2-7.2.0}/PKG-INFO +8 -13
  3. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/README.md +7 -12
  4. python_hcl2-7.2.0/hcl2/builder.py +86 -0
  5. python_hcl2-7.2.0/hcl2/const.py +4 -0
  6. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/hcl2/hcl2.lark +11 -8
  7. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/hcl2/reconstructor.py +70 -37
  8. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/hcl2/transformer.py +31 -15
  9. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/hcl2/version.py +2 -2
  10. {python_hcl2-7.0.1 → python_hcl2-7.2.0/python_hcl2.egg-info}/PKG-INFO +8 -13
  11. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/python_hcl2.egg-info/SOURCES.txt +1 -0
  12. python_hcl2-7.0.1/hcl2/builder.py +0 -63
  13. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/.codacy.yml +0 -0
  14. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/.coveragerc +0 -0
  15. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/.github/CODEOWNERS +0 -0
  16. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/.github/ISSUE_TEMPLATE/hcl2-parsing-error.md +0 -0
  17. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/.github/workflows/codeql-analysis.yml +0 -0
  18. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/.github/workflows/pr_check.yml +0 -0
  19. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/.github/workflows/publish.yml +0 -0
  20. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/.gitignore +0 -0
  21. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/.pre-commit-config.yaml +0 -0
  22. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/.yamllint.yml +0 -0
  23. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/LICENSE +0 -0
  24. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/MANIFEST.in +0 -0
  25. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/bin/terraform_test +0 -0
  26. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/hcl2/__init__.py +0 -0
  27. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/hcl2/__main__.py +0 -0
  28. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/hcl2/api.py +0 -0
  29. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/hcl2/parser.py +0 -0
  30. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/hcl2/py.typed +0 -0
  31. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/mypy.ini +0 -0
  32. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/pylintrc +0 -0
  33. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/pyproject.toml +0 -0
  34. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/python_hcl2.egg-info/dependency_links.txt +0 -0
  35. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/python_hcl2.egg-info/entry_points.txt +0 -0
  36. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/python_hcl2.egg-info/not-zip-safe +0 -0
  37. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/python_hcl2.egg-info/requires.txt +0 -0
  38. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/python_hcl2.egg-info/top_level.txt +0 -0
  39. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/reports/.gitignore +0 -0
  40. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/requirements.txt +0 -0
  41. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/setup.cfg +0 -0
  42. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/test-requirements.txt +0 -0
  43. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/tox.ini +0 -0
  44. {python_hcl2-7.0.1 → python_hcl2-7.2.0}/tree-to-hcl2-reconstruction.md +0 -0
@@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
6
6
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## \[Unreleased\]
9
+
10
+ - Nothing Yet
11
+
12
+ ## \[7.2.0\] - 2025-04-24
13
+
14
+ ### Added
15
+
16
+ - Possibility to parse deeply nested interpolations (formerly a Limitation), Thanks again, @weaversam8 ([#223](https://github.com/amplify-education/python-hcl2/pull/223))
17
+
18
+ ### Fixed
19
+
20
+ - Issue parsing ellipsis in a separate line within `for` expression ([#221](https://github.com/amplify-education/python-hcl2/pull/221))
21
+ - Issue parsing inline expression as an object key; **see Limitations in README.md** ([#222](https://github.com/amplify-education/python-hcl2/pull/222))
22
+ - Preserve literals of e-notation floats in parsing and reconstruction. Thanks, @eranor ([#226](https://github.com/amplify-education/python-hcl2/pull/226))
23
+
24
+ ## \[7.1.0\] - 2025-04-10
25
+
26
+ ### Added
27
+
28
+ - `hcl2.builder.Builder` - nested blocks support ([#214](https://github.com/amplify-education/python-hcl2/pull/214))
29
+
30
+ ### Fixed
31
+
32
+ - Issue parsing parenthesesed identifier (reference) as an object key ([#212](https://github.com/amplify-education/python-hcl2/pull/212))
33
+ - Issue discarding empty lists when transforming python dictionary into Lark Tree ([#216](https://github.com/amplify-education/python-hcl2/pull/216))
34
+
8
35
  ## \[7.0.1\] - 2025-03-31
9
36
 
10
37
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-hcl2
3
- Version: 7.0.1
3
+ Version: 7.2.0
4
4
  Summary: A parser for HCL2
5
5
  Author-email: Amplify Education <github@amplify.com>
6
6
  License: MIT
@@ -114,20 +114,15 @@ We’ll try to answer any PR’s promptly.
114
114
 
115
115
  ## Limitations
116
116
 
117
- ### Error parsing string interpolations nested more than 2 times
117
+ ### Using inline expression as an object key
118
118
 
119
- - Parsing following example is expected to throw out an exception and fail:
119
+ - Object key can be an expression as long as it is wrapped in parentheses:
120
120
  ```terraform
121
121
  locals {
122
- foo = "foo"
123
- name = "prefix1-${"prefix2-${"${local.foo}-bar"}"}" //should interpolate into "prefix1-prefix2-foo-bar" but fails
124
- }
125
- ```
126
- We recommend working around this by modifying the configuration in the following manner:
127
- ```terraform
128
- locals {
129
- foo = "foo"
130
- foo_bar = "${local.foo}-bar"
131
- name = "prefix1-${"prefix2-${local.foo_bar}"}" //interpolates into "prefix1-prefix2-foo-bar"
122
+ foo = "bar"
123
+ baz = {
124
+ (format("key_prefix_%s", local.foo)) : "value"
125
+ # format("key_prefix_%s", local.foo) : "value" this will fail
126
+ }
132
127
  }
133
128
  ```
@@ -87,20 +87,15 @@ We’ll try to answer any PR’s promptly.
87
87
 
88
88
  ## Limitations
89
89
 
90
- ### Error parsing string interpolations nested more than 2 times
90
+ ### Using inline expression as an object key
91
91
 
92
- - Parsing following example is expected to throw out an exception and fail:
92
+ - Object key can be an expression as long as it is wrapped in parentheses:
93
93
  ```terraform
94
94
  locals {
95
- foo = "foo"
96
- name = "prefix1-${"prefix2-${"${local.foo}-bar"}"}" //should interpolate into "prefix1-prefix2-foo-bar" but fails
97
- }
98
- ```
99
- We recommend working around this by modifying the configuration in the following manner:
100
- ```terraform
101
- locals {
102
- foo = "foo"
103
- foo_bar = "${local.foo}-bar"
104
- name = "prefix1-${"prefix2-${local.foo_bar}"}" //interpolates into "prefix1-prefix2-foo-bar"
95
+ foo = "bar"
96
+ baz = {
97
+ (format("key_prefix_%s", local.foo)) : "value"
98
+ # format("key_prefix_%s", local.foo) : "value" this will fail
99
+ }
105
100
  }
106
101
  ```
@@ -0,0 +1,86 @@
1
+ """A utility class for constructing HCL documents from Python code."""
2
+ from typing import List, Optional
3
+
4
+ from collections import defaultdict
5
+
6
+ from hcl2.const import START_LINE_KEY, END_LINE_KEY
7
+
8
+
9
+ class Builder:
10
+ """
11
+ The `hcl2.Builder` class produces a dictionary that should be identical to the
12
+ output of `hcl2.load(example_file, with_meta=True)`. The `with_meta` keyword
13
+ argument is important here. HCL "blocks" in the Python dictionary are
14
+ identified by the presence of `__start_line__` and `__end_line__` metadata
15
+ within them. The `Builder` class handles adding that metadata. If that metadata
16
+ is missing, the `hcl2.reconstructor.HCLReverseTransformer` class fails to
17
+ identify what is a block and what is just an attribute with an object value.
18
+ """
19
+
20
+ def __init__(self, attributes: Optional[dict] = None):
21
+ self.blocks: dict = defaultdict(list)
22
+ self.attributes = attributes or {}
23
+
24
+ def block(
25
+ self,
26
+ block_type: str,
27
+ labels: Optional[List[str]] = None,
28
+ __nested_builder__: Optional["Builder"] = None,
29
+ **attributes
30
+ ) -> "Builder":
31
+ """Create a block within this HCL document."""
32
+
33
+ if __nested_builder__ is self:
34
+ raise ValueError(
35
+ "__nested__builder__ cannot be the same instance as instance this method is called on"
36
+ )
37
+
38
+ labels = labels or []
39
+ block = Builder(attributes)
40
+
41
+ # store the block in the document
42
+ self.blocks[block_type].append((labels.copy(), block, __nested_builder__))
43
+
44
+ return block
45
+
46
+ def build(self):
47
+ """Return the Python dictionary for this HCL document."""
48
+ body = defaultdict(list)
49
+
50
+ body.update(
51
+ {
52
+ START_LINE_KEY: -1,
53
+ END_LINE_KEY: -1,
54
+ **self.attributes,
55
+ }
56
+ )
57
+
58
+ for block_type, blocks in self.blocks.items():
59
+
60
+ for labels, block_builder, nested_blocks in blocks:
61
+ # build the sub-block
62
+ block = block_builder.build()
63
+
64
+ if nested_blocks:
65
+ self._add_nested_blocks(block, nested_blocks)
66
+
67
+ # apply any labels
68
+ for label in reversed(labels):
69
+ block = {label: block}
70
+
71
+ # store it in the body
72
+ body[block_type].append(block)
73
+
74
+ return body
75
+
76
+ def _add_nested_blocks(
77
+ self, block: dict, nested_blocks_builder: "Builder"
78
+ ) -> "dict":
79
+ """Add nested blocks defined within another `Builder` instance to the `block` dictionary"""
80
+ nested_block = nested_blocks_builder.build()
81
+ for key, value in nested_block.items():
82
+ if key not in (START_LINE_KEY, END_LINE_KEY):
83
+ if key not in block.keys():
84
+ block[key] = []
85
+ block[key].extend(value)
86
+ return block
@@ -0,0 +1,4 @@
1
+ """Module for various constants used across the library"""
2
+
3
+ START_LINE_KEY = "__start_line__"
4
+ END_LINE_KEY = "__end_line__"
@@ -1,7 +1,7 @@
1
1
  start : body
2
2
  body : (new_line_or_comment? (attribute | block))* new_line_or_comment?
3
3
  attribute : identifier EQ expression
4
- block : identifier (identifier | STRING_LIT)* new_line_or_comment? "{" body "}"
4
+ block : identifier (identifier | STRING_LIT | string_with_interpolation)* new_line_or_comment? "{" body "}"
5
5
  new_line_or_comment: ( NL_OR_COMMENT )+
6
6
  NL_OR_COMMENT: /\n[ \t]*/ | /#.*\n/ | /\/\/.*\n/ | /\/\*(.|\n)*?(\*\/)/
7
7
 
@@ -39,11 +39,13 @@ LPAR : "("
39
39
  RPAR : ")"
40
40
  COMMA : ","
41
41
  DOT : "."
42
+ COLON : ":"
42
43
 
43
44
  expr_term : LPAR new_line_or_comment? expression new_line_or_comment? RPAR
44
45
  | float_lit
45
46
  | int_lit
46
47
  | STRING_LIT
48
+ | string_with_interpolation
47
49
  | tuple
48
50
  | object
49
51
  | function_call
@@ -58,10 +60,10 @@ expr_term : LPAR new_line_or_comment? expression new_line_or_comment? RPAR
58
60
  | for_tuple_expr
59
61
  | for_object_expr
60
62
 
61
- STRING_LIT : "\"" (STRING_CHARS | INTERPOLATION)* "\""
62
- STRING_CHARS : /(?:(?!\${)([^"\\]|\\.))+/+ // any character except '"" unless inside a interpolation string
63
- NESTED_INTERPOLATION : "${" /[^}]+/ "}"
64
- INTERPOLATION : "${" (/(?:(?!\${)([^}]))+/ | NESTED_INTERPOLATION)+ "}"
63
+ STRING_LIT : "\"" STRING_CHARS? "\""
64
+ STRING_CHARS : /(?:(?!\${)([^"\\]|\\.|\$\$))+/ // any character except '"', including escaped $$
65
+ string_with_interpolation: "\"" (STRING_CHARS)* interpolation_maybe_nested (STRING_CHARS | interpolation_maybe_nested)* "\""
66
+ interpolation_maybe_nested: "${" expression "}"
65
67
 
66
68
 
67
69
  int_lit : NEGATIVE_DECIMAL? DECIMAL+ | NEGATIVE_DECIMAL+
@@ -74,8 +76,9 @@ EQ : /[ \t]*=(?!=|>)/
74
76
 
75
77
  tuple : "[" (new_line_or_comment* expression new_line_or_comment* ",")* (new_line_or_comment* expression)? new_line_or_comment* "]"
76
78
  object : "{" new_line_or_comment? (new_line_or_comment* (object_elem | (object_elem COMMA)) new_line_or_comment*)* "}"
77
- object_elem : object_elem_key ( EQ | ":") expression
78
- object_elem_key : float_lit | int_lit | identifier | STRING_LIT | object_elem_key_dot_accessor
79
+ object_elem : object_elem_key ( EQ | COLON ) expression
80
+ object_elem_key : float_lit | int_lit | identifier | STRING_LIT | object_elem_key_dot_accessor | object_elem_key_expression
81
+ object_elem_key_expression : LPAR expression RPAR
79
82
  object_elem_key_dot_accessor : identifier (DOT identifier)+
80
83
 
81
84
  heredoc_template : /<<(?P<heredoc>[a-zA-Z][a-zA-Z0-9._-]+)\n?(?:.|\n)*?\n\s*(?P=heredoc)\n/
@@ -97,7 +100,7 @@ full_splat : "[*]" (get_attr | index)*
97
100
 
98
101
  FOR_OBJECT_ARROW : "=>"
99
102
  !for_tuple_expr : "[" new_line_or_comment? for_intro new_line_or_comment? expression new_line_or_comment? for_cond? new_line_or_comment? "]"
100
- !for_object_expr : "{" new_line_or_comment? for_intro new_line_or_comment? expression FOR_OBJECT_ARROW new_line_or_comment? expression "..."? new_line_or_comment? for_cond? new_line_or_comment? "}"
103
+ !for_object_expr : "{" new_line_or_comment? for_intro new_line_or_comment? expression FOR_OBJECT_ARROW new_line_or_comment? expression new_line_or_comment? "..."? new_line_or_comment? for_cond? new_line_or_comment? "}"
101
104
  !for_intro : "for" new_line_or_comment? identifier ("," identifier new_line_or_comment?)? new_line_or_comment? "in" new_line_or_comment? expression new_line_or_comment? ":" new_line_or_comment?
102
105
  !for_cond : "if" new_line_or_comment? expression
103
106
 
@@ -10,6 +10,8 @@ from lark.lexer import Token, PatternStr, TerminalDef
10
10
  from lark.reconstruct import Reconstructor
11
11
  from lark.tree_matcher import is_discarded_terminal
12
12
  from lark.visitors import Transformer_InPlace
13
+
14
+ from hcl2.const import START_LINE_KEY, END_LINE_KEY
13
15
  from hcl2.parser import reconstruction_parser
14
16
 
15
17
 
@@ -258,6 +260,7 @@ class HCLReconstructor(Reconstructor):
258
260
  Terminal("STRING_LIT"),
259
261
  Terminal("DECIMAL"),
260
262
  Terminal("NAME"),
263
+ Terminal("NEGATIVE_DECIMAL"),
261
264
  ]:
262
265
  return True
263
266
 
@@ -395,46 +398,38 @@ class HCLReverseTransformer:
395
398
 
396
399
  return True
397
400
 
401
+ @classmethod
402
+ def _unwrap_interpolation(cls, value: str) -> str:
403
+ if cls._is_string_wrapped_tf(value):
404
+ return value[2:-1]
405
+ return value
406
+
398
407
  def _newline(self, level: int, count: int = 1) -> Tree:
399
408
  return Tree(
400
409
  Token("RULE", "new_line_or_comment"),
401
410
  [Token("NL_OR_COMMENT", f"\n{' ' * level}") for _ in range(count)],
402
411
  )
403
412
 
404
- # rules: the value of a block is always an array of dicts,
405
- # the key is the block type
406
- def _list_is_a_block(self, value: list) -> bool:
407
- for obj in value:
408
- if not self._dict_is_a_block(obj):
409
- return False
410
-
411
- return True
412
-
413
- def _dict_is_a_block(self, sub_obj: Any) -> bool:
414
- # if the list doesn't contain dictionaries, it's not a block
415
- if not isinstance(sub_obj, dict):
416
- return False
413
+ def _is_block(self, value: Any) -> bool:
414
+ if isinstance(value, dict):
415
+ block_body = value
416
+ if START_LINE_KEY in block_body.keys() or END_LINE_KEY in block_body.keys():
417
+ return True
417
418
 
418
- # if the sub object has "start_line" and "end_line" metadata,
419
- # the block itself is unlabeled, but it is a block
420
- if "__start_line__" in sub_obj.keys() or "__end_line__" in sub_obj.keys():
421
- return True
419
+ try:
420
+ # if block is labeled, actual body might be nested
421
+ # pylint: disable=W0612
422
+ block_label, block_body = next(iter(value.items()))
423
+ except StopIteration:
424
+ # no more potential labels = nothing more to check
425
+ return False
422
426
 
423
- # if the objects in the array have no metadata and more than 2 keys and
424
- # no metadata, it's just an array of objects, not a block
425
- if len(list(sub_obj)) != 1:
426
- return False
427
+ return self._is_block(block_body)
427
428
 
428
- # if the sub object has a single string key whose value is an object,
429
- # it _could_ be a labeled block... but we'd have to check if the sub
430
- # object is a block (recurse)
431
- label = list(sub_obj)[0]
432
- sub_sub_obj = sub_obj[label]
433
- if self._dict_is_a_block(sub_sub_obj):
434
- return True
429
+ if isinstance(value, list):
430
+ if len(value) > 0:
431
+ return self._is_block(value[0])
435
432
 
436
- # if the objects in the array have a single key whose child is not a
437
- # block, the array is just an array of objects, not a block
438
433
  return False
439
434
 
440
435
  def _calculate_block_labels(self, block: dict) -> Tuple[List[str], dict]:
@@ -448,8 +443,8 @@ class HCLReverseTransformer:
448
443
 
449
444
  # __start_line__ and __end_line__ metadata are not labels
450
445
  if (
451
- "__start_line__" in potential_body.keys()
452
- or "__end_line__" in potential_body.keys()
446
+ START_LINE_KEY in potential_body.keys()
447
+ or END_LINE_KEY in potential_body.keys()
453
448
  ):
454
449
  return [curr_label], potential_body
455
450
 
@@ -457,6 +452,7 @@ class HCLReverseTransformer:
457
452
  next_label, block_body = self._calculate_block_labels(potential_body)
458
453
  return [curr_label] + next_label, block_body
459
454
 
455
+ # pylint:disable=R0914
460
456
  def _transform_dict_to_body(self, hcl_dict: dict, level: int) -> Tree:
461
457
  # we add a newline at the top of a body within a block, not the root body
462
458
  # >2 here is to ignore the __start_line__ and __end_line__ metadata
@@ -467,14 +463,14 @@ class HCLReverseTransformer:
467
463
 
468
464
  # iterate through each attribute or sub-block of this block
469
465
  for key, value in hcl_dict.items():
470
- if key in ["__start_line__", "__end_line__"]:
466
+ if key in [START_LINE_KEY, END_LINE_KEY]:
471
467
  continue
472
468
 
473
469
  # construct the identifier, whether that be a block type name or an attribute key
474
470
  identifier_name = self._name_to_identifier(key)
475
471
 
476
472
  # first, check whether the value is a "block"
477
- if isinstance(value, list) and self._list_is_a_block(value):
473
+ if self._is_block(value):
478
474
  for block_v in value:
479
475
  block_labels, block_body_dict = self._calculate_block_labels(
480
476
  block_v
@@ -493,7 +489,12 @@ class HCLReverseTransformer:
493
489
  [identifier_name] + block_label_tokens + [block_body],
494
490
  )
495
491
  children.append(block)
496
- children.append(self._newline(level, count=2))
492
+ # add empty line after block
493
+ new_line = self._newline(level - 1)
494
+ # add empty line with indentation for next element in the block
495
+ new_line.children.append(self._newline(level).children[0])
496
+
497
+ children.append(new_line)
497
498
 
498
499
  # if the value isn't a block, it's an attribute
499
500
  else:
@@ -517,7 +518,7 @@ class HCLReverseTransformer:
517
518
 
518
519
  return Tree(Token("RULE", "body"), children)
519
520
 
520
- # pylint: disable=too-many-branches, too-many-return-statements
521
+ # pylint: disable=too-many-branches, too-many-return-statements too-many-statements
521
522
  def _transform_value_to_expr_term(self, value, level) -> Union[Token, Tree]:
522
523
  """Transforms a value from a dictionary into an "expr_term" (a value in HCL2)
523
524
 
@@ -556,10 +557,11 @@ class HCLReverseTransformer:
556
557
 
557
558
  # iterate through the items and add them to the object
558
559
  for i, (k, dict_v) in enumerate(value.items()):
559
- if k in ["__start_line__", "__end_line__"]:
560
+ if k in [START_LINE_KEY, END_LINE_KEY]:
560
561
  continue
561
562
 
562
563
  value_expr_term = self._transform_value_to_expr_term(dict_v, level + 1)
564
+ k = self._unwrap_interpolation(k)
563
565
  elements.append(
564
566
  Tree(
565
567
  Token("RULE", "object_elem"),
@@ -607,6 +609,37 @@ class HCLReverseTransformer:
607
609
  ],
608
610
  )
609
611
 
612
+ if isinstance(value, float):
613
+ value = str(value)
614
+ literal = []
615
+
616
+ if value[0] == "-":
617
+ # pop two first chars - minus and a digit
618
+ literal.append(Token("NEGATIVE_DECIMAL", value[:2]))
619
+ value = value[2:]
620
+
621
+ while value != "":
622
+ char = value[0]
623
+
624
+ if char == ".":
625
+ # current char marks beginning of decimal part: pop all remaining chars and end the loop
626
+ literal.append(Token("DOT", char))
627
+ literal.extend(Token("DECIMAL", char) for char in value[1:])
628
+ break
629
+
630
+ if char == "e":
631
+ # current char marks beginning of e-notation: pop all remaining chars and end the loop
632
+ literal.append(Token("EXP_MARK", value))
633
+ break
634
+
635
+ literal.append(Token("DECIMAL", char))
636
+ value = value[1:]
637
+
638
+ return Tree(
639
+ Token("RULE", "expr_term"),
640
+ [Tree(Token("RULE", "float_lit"), literal)],
641
+ )
642
+
610
643
  # store strings as single literals
611
644
  if isinstance(value, str):
612
645
  # potentially unpack a complex syntax structure
@@ -44,7 +44,10 @@ class DictTransformer(Transformer):
44
44
  super().__init__()
45
45
 
46
46
  def float_lit(self, args: List) -> float:
47
- return float("".join([self.to_tf_inline(arg) for arg in args]))
47
+ value = "".join([self.to_tf_inline(arg) for arg in args])
48
+ if "e" in value:
49
+ return self.to_string_dollar(value)
50
+ return float(value)
48
51
 
49
52
  def int_lit(self, args: List) -> int:
50
53
  return int("".join([self.to_tf_inline(arg) for arg in args]))
@@ -98,11 +101,9 @@ class DictTransformer(Transformer):
98
101
  def object_elem(self, args: List) -> Dict:
99
102
  # This returns a dict with a single key/value pair to make it easier to merge these
100
103
  # into a bigger dict that is returned by the "object" function
104
+
101
105
  key = self.strip_quotes(str(args[0].children[0]))
102
- if len(args) == 3:
103
- value = args[2]
104
- else:
105
- value = args[1]
106
+ value = args[2]
106
107
 
107
108
  value = self.to_string_dollar(value)
108
109
  return {key: value}
@@ -110,6 +111,9 @@ class DictTransformer(Transformer):
110
111
  def object_elem_key_dot_accessor(self, args: List) -> str:
111
112
  return "".join(args)
112
113
 
114
+ def object_elem_key_expression(self, args: List) -> str:
115
+ return self.to_string_dollar("".join(args))
116
+
113
117
  def object(self, args: List) -> Dict:
114
118
  args = self.strip_new_line_tokens(args)
115
119
  result: Dict[str, Any] = {}
@@ -176,7 +180,9 @@ class DictTransformer(Transformer):
176
180
  return f"{args[0]} ? {args[1]} : {args[2]}"
177
181
 
178
182
  def binary_op(self, args: List) -> str:
179
- return " ".join([self.to_tf_inline(arg) for arg in args])
183
+ return " ".join(
184
+ [self.unwrap_string_dollar(self.to_tf_inline(arg)) for arg in args]
185
+ )
180
186
 
181
187
  def unary_op(self, args: List) -> str:
182
188
  args = self.process_nulls(args)
@@ -303,21 +309,31 @@ class DictTransformer(Transformer):
303
309
  """
304
310
  return [arg for arg in args if arg != "\n" and arg is not Discard]
305
311
 
312
+ def is_string_dollar(self, value: str) -> bool:
313
+ if not isinstance(value, str):
314
+ return False
315
+ return value.startswith("${") and value.endswith("}")
316
+
306
317
  def to_string_dollar(self, value: Any) -> Any:
307
318
  """Wrap a string in ${ and }"""
308
- if isinstance(value, str):
319
+ if not isinstance(value, str):
320
+ return value
309
321
  # if it's already wrapped, pass it unmodified
310
- if value.startswith("${") and value.endswith("}"):
311
- return value
322
+ if self.is_string_dollar(value):
323
+ return value
312
324
 
313
- if value.startswith('"') and value.endswith('"'):
314
- value = str(value)[1:-1]
315
- return self.process_escape_sequences(value)
325
+ if value.startswith('"') and value.endswith('"'):
326
+ value = str(value)[1:-1]
327
+ return self.process_escape_sequences(value)
328
+
329
+ if self.is_type_keyword(value):
330
+ return value
316
331
 
317
- if self.is_type_keyword(value):
318
- return value
332
+ return f"${{{value}}}"
319
333
 
320
- return f"${{{value}}}"
334
+ def unwrap_string_dollar(self, value: str):
335
+ if self.is_string_dollar(value):
336
+ return value[2:-1]
321
337
  return value
322
338
 
323
339
  def strip_quotes(self, value: Any) -> Any:
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '7.0.1'
21
- __version_tuple__ = version_tuple = (7, 0, 1)
20
+ __version__ = version = '7.2.0'
21
+ __version_tuple__ = version_tuple = (7, 2, 0)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-hcl2
3
- Version: 7.0.1
3
+ Version: 7.2.0
4
4
  Summary: A parser for HCL2
5
5
  Author-email: Amplify Education <github@amplify.com>
6
6
  License: MIT
@@ -114,20 +114,15 @@ We’ll try to answer any PR’s promptly.
114
114
 
115
115
  ## Limitations
116
116
 
117
- ### Error parsing string interpolations nested more than 2 times
117
+ ### Using inline expression as an object key
118
118
 
119
- - Parsing following example is expected to throw out an exception and fail:
119
+ - Object key can be an expression as long as it is wrapped in parentheses:
120
120
  ```terraform
121
121
  locals {
122
- foo = "foo"
123
- name = "prefix1-${"prefix2-${"${local.foo}-bar"}"}" //should interpolate into "prefix1-prefix2-foo-bar" but fails
124
- }
125
- ```
126
- We recommend working around this by modifying the configuration in the following manner:
127
- ```terraform
128
- locals {
129
- foo = "foo"
130
- foo_bar = "${local.foo}-bar"
131
- name = "prefix1-${"prefix2-${local.foo_bar}"}" //interpolates into "prefix1-prefix2-foo-bar"
122
+ foo = "bar"
123
+ baz = {
124
+ (format("key_prefix_%s", local.foo)) : "value"
125
+ # format("key_prefix_%s", local.foo) : "value" this will fail
126
+ }
132
127
  }
133
128
  ```
@@ -24,6 +24,7 @@ hcl2/__init__.py
24
24
  hcl2/__main__.py
25
25
  hcl2/api.py
26
26
  hcl2/builder.py
27
+ hcl2/const.py
27
28
  hcl2/hcl2.lark
28
29
  hcl2/parser.py
29
30
  hcl2/py.typed
@@ -1,63 +0,0 @@
1
- """A utility class for constructing HCL documents from Python code."""
2
-
3
- from typing import List, Optional
4
-
5
-
6
- class Builder:
7
- """
8
- The `hcl2.Builder` class produces a dictionary that should be identical to the
9
- output of `hcl2.load(example_file, with_meta=True)`. The `with_meta` keyword
10
- argument is important here. HCL "blocks" in the Python dictionary are
11
- identified by the presence of `__start_line__` and `__end_line__` metadata
12
- within them. The `Builder` class handles adding that metadata. If that metadata
13
- is missing, the `hcl2.reconstructor.HCLReverseTransformer` class fails to
14
- identify what is a block and what is just an attribute with an object value.
15
- """
16
-
17
- def __init__(self, attributes: Optional[dict] = None):
18
- self.blocks: dict = {}
19
- self.attributes = attributes or {}
20
-
21
- def block(
22
- self, block_type: str, labels: Optional[List[str]] = None, **attributes
23
- ) -> "Builder":
24
- """Create a block within this HCL document."""
25
- labels = labels or []
26
- block = Builder(attributes)
27
-
28
- # initialize a holder for blocks of that type
29
- if block_type not in self.blocks:
30
- self.blocks[block_type] = []
31
-
32
- # store the block in the document
33
- self.blocks[block_type].append((labels.copy(), block))
34
-
35
- return block
36
-
37
- def build(self):
38
- """Return the Python dictionary for this HCL document."""
39
- body = {
40
- "__start_line__": -1,
41
- "__end_line__": -1,
42
- **self.attributes,
43
- }
44
-
45
- for block_type, blocks in self.blocks.items():
46
-
47
- # initialize a holder for blocks of that type
48
- if block_type not in body:
49
- body[block_type] = []
50
-
51
- for labels, block_builder in blocks:
52
- # build the sub-block
53
- block = block_builder.build()
54
-
55
- # apply any labels
56
- labels.reverse()
57
- for label in labels:
58
- block = {label: block}
59
-
60
- # store it in the body
61
- body[block_type].append(block)
62
-
63
- return body
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes