Skip to content

Commit 3ef6ffd

Browse files
hcl2.builder.Builder - nested blocks support (#214)
1 parent b43ccf0 commit 3ef6ffd

File tree

7 files changed

+118
-37
lines changed

7 files changed

+118
-37
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
77

88
## \[Unreleased\]
99

10+
### Added
11+
12+
- `hcl2.builder.Builder` - nested blocks support ([#214](https://github.com/amplify-education/python-hcl2/pull/214))
13+
1014
### Fixed
1115

1216
- Issue parsing parenthesesed identifier (reference) as an object key ([#212](https://github.com/amplify-education/python-hcl2/pull/212))

hcl2/builder.py

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
"""A utility class for constructing HCL documents from Python code."""
2-
32
from typing import List, Optional
43

4+
from collections import defaultdict
5+
6+
from hcl2.const import START_LINE_KEY, END_LINE_KEY
7+
58

69
class Builder:
710
"""
@@ -15,49 +18,69 @@ class Builder:
1518
"""
1619

1720
def __init__(self, attributes: Optional[dict] = None):
18-
self.blocks: dict = {}
21+
self.blocks: dict = defaultdict(list)
1922
self.attributes = attributes or {}
2023

2124
def block(
22-
self, block_type: str, labels: Optional[List[str]] = None, **attributes
25+
self,
26+
block_type: str,
27+
labels: Optional[List[str]] = None,
28+
__nested_builder__: Optional["Builder"] = None,
29+
**attributes
2330
) -> "Builder":
2431
"""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+
2538
labels = labels or []
2639
block = Builder(attributes)
2740

28-
# initialize a holder for blocks of that type
29-
if block_type not in self.blocks:
30-
self.blocks[block_type] = []
31-
3241
# store the block in the document
33-
self.blocks[block_type].append((labels.copy(), block))
42+
self.blocks[block_type].append((labels.copy(), block, __nested_builder__))
3443

3544
return block
3645

3746
def build(self):
3847
"""Return the Python dictionary for this HCL document."""
39-
body = {
40-
"__start_line__": -1,
41-
"__end_line__": -1,
42-
**self.attributes,
43-
}
48+
body = defaultdict(list)
4449

45-
for block_type, blocks in self.blocks.items():
50+
body.update(
51+
{
52+
START_LINE_KEY: -1,
53+
END_LINE_KEY: -1,
54+
**self.attributes,
55+
}
56+
)
4657

47-
# initialize a holder for blocks of that type
48-
if block_type not in body:
49-
body[block_type] = []
58+
for block_type, blocks in self.blocks.items():
5059

51-
for labels, block_builder in blocks:
60+
for labels, block_builder, nested_blocks in blocks:
5261
# build the sub-block
5362
block = block_builder.build()
5463

64+
if nested_blocks:
65+
self._add_nested_blocks(block, nested_blocks)
66+
5567
# apply any labels
56-
labels.reverse()
57-
for label in labels:
68+
for label in reversed(labels):
5869
block = {label: block}
5970

6071
# store it in the body
6172
body[block_type].append(block)
6273

6374
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

hcl2/const.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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__"

hcl2/reconstructor.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from lark.reconstruct import Reconstructor
1111
from lark.tree_matcher import is_discarded_terminal
1212
from lark.visitors import Transformer_InPlace
13+
14+
from hcl2.const import START_LINE_KEY, END_LINE_KEY
1315
from hcl2.parser import reconstruction_parser
1416

1517

@@ -423,7 +425,7 @@ def _dict_is_a_block(self, sub_obj: Any) -> bool:
423425

424426
# if the sub object has "start_line" and "end_line" metadata,
425427
# the block itself is unlabeled, but it is a block
426-
if "__start_line__" in sub_obj.keys() or "__end_line__" in sub_obj.keys():
428+
if START_LINE_KEY in sub_obj.keys() or END_LINE_KEY in sub_obj.keys():
427429
return True
428430

429431
# if the objects in the array have no metadata and more than 2 keys and
@@ -454,15 +456,16 @@ def _calculate_block_labels(self, block: dict) -> Tuple[List[str], dict]:
454456

455457
# __start_line__ and __end_line__ metadata are not labels
456458
if (
457-
"__start_line__" in potential_body.keys()
458-
or "__end_line__" in potential_body.keys()
459+
START_LINE_KEY in potential_body.keys()
460+
or END_LINE_KEY in potential_body.keys()
459461
):
460462
return [curr_label], potential_body
461463

462464
# recurse and append the label
463465
next_label, block_body = self._calculate_block_labels(potential_body)
464466
return [curr_label] + next_label, block_body
465467

468+
# pylint:disable=R0914
466469
def _transform_dict_to_body(self, hcl_dict: dict, level: int) -> Tree:
467470
# we add a newline at the top of a body within a block, not the root body
468471
# >2 here is to ignore the __start_line__ and __end_line__ metadata
@@ -473,7 +476,7 @@ def _transform_dict_to_body(self, hcl_dict: dict, level: int) -> Tree:
473476

474477
# iterate through each attribute or sub-block of this block
475478
for key, value in hcl_dict.items():
476-
if key in ["__start_line__", "__end_line__"]:
479+
if key in [START_LINE_KEY, END_LINE_KEY]:
477480
continue
478481

479482
# construct the identifier, whether that be a block type name or an attribute key
@@ -499,7 +502,12 @@ def _transform_dict_to_body(self, hcl_dict: dict, level: int) -> Tree:
499502
[identifier_name] + block_label_tokens + [block_body],
500503
)
501504
children.append(block)
502-
children.append(self._newline(level, count=2))
505+
# add empty line after block
506+
new_line = self._newline(level - 1)
507+
# add empty line with indentation for next element in the block
508+
new_line.children.append(self._newline(level).children[0])
509+
510+
children.append(new_line)
503511

504512
# if the value isn't a block, it's an attribute
505513
else:
@@ -562,7 +570,7 @@ def _transform_value_to_expr_term(self, value, level) -> Union[Token, Tree]:
562570

563571
# iterate through the items and add them to the object
564572
for i, (k, dict_v) in enumerate(value.items()):
565-
if k in ["__start_line__", "__end_line__"]:
573+
if k in [START_LINE_KEY, END_LINE_KEY]:
566574
continue
567575

568576
value_expr_term = self._transform_value_to_expr_term(dict_v, level + 1)
Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,34 @@
11
{
2-
"block": [
3-
{
4-
"a": 1
5-
},
6-
{
7-
"label": {
8-
"b": 2
9-
}
10-
}
2+
"block": [
3+
{
4+
"a": 1
5+
},
6+
{
7+
"label": {
8+
"b": 2,
9+
"nested_block_1": [
10+
{
11+
"a": {
12+
"foo": "bar"
13+
}
14+
},
15+
{
16+
"a": {
17+
"b": {
18+
"bar": "foo"
19+
}
20+
}
21+
},
22+
{
23+
"foobar": "barfoo"
24+
}
25+
],
26+
"nested_block_2": [
27+
{
28+
"barfoo": "foobar"
29+
}
1130
]
31+
}
32+
}
33+
]
1234
}

test/helpers/terraform-config/blocks.tf

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,19 @@ block {
44

55
block "label" {
66
b = 2
7+
nested_block_1 "a" {
8+
foo = "bar"
9+
}
10+
11+
nested_block_1 "a" "b" {
12+
bar = "foo"
13+
}
14+
15+
nested_block_1 {
16+
foobar = "barfoo"
17+
}
18+
19+
nested_block_2 {
20+
barfoo = "foobar"
21+
}
722
}

test/unit/test_builder.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,15 @@ class TestBuilder(TestCase):
2222
maxDiff = None
2323

2424
def test_build_blocks_tf(self):
25-
builder = hcl2.Builder()
25+
nested_builder = hcl2.Builder()
26+
nested_builder.block("nested_block_1", ["a"], foo="bar")
27+
nested_builder.block("nested_block_1", ["a", "b"], bar="foo")
28+
nested_builder.block("nested_block_1", foobar="barfoo")
29+
nested_builder.block("nested_block_2", barfoo="foobar")
2630

31+
builder = hcl2.Builder()
2732
builder.block("block", a=1)
28-
builder.block("block", ["label"], b=2)
33+
builder.block("block", ["label"], __nested_builder__=nested_builder, b=2)
2934

3035
self.compare_filenames(builder, "blocks.tf")
3136

0 commit comments

Comments
 (0)