python-hcl2 7.0.1__tar.gz → 7.1.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.
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/CHANGELOG.md +15 -0
- {python_hcl2-7.0.1/python_hcl2.egg-info → python_hcl2-7.1.0}/PKG-INFO +1 -1
- python_hcl2-7.1.0/hcl2/builder.py +86 -0
- python_hcl2-7.1.0/hcl2/const.py +4 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/hcl2/hcl2.lark +2 -1
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/hcl2/reconstructor.py +40 -36
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/hcl2/transformer.py +7 -4
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/hcl2/version.py +2 -2
- {python_hcl2-7.0.1 → python_hcl2-7.1.0/python_hcl2.egg-info}/PKG-INFO +1 -1
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/python_hcl2.egg-info/SOURCES.txt +1 -0
- python_hcl2-7.0.1/hcl2/builder.py +0 -63
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/.codacy.yml +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/.coveragerc +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/.github/CODEOWNERS +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/.github/ISSUE_TEMPLATE/hcl2-parsing-error.md +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/.github/workflows/codeql-analysis.yml +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/.github/workflows/pr_check.yml +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/.github/workflows/publish.yml +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/.gitignore +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/.pre-commit-config.yaml +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/.yamllint.yml +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/LICENSE +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/MANIFEST.in +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/README.md +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/bin/terraform_test +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/hcl2/__init__.py +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/hcl2/__main__.py +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/hcl2/api.py +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/hcl2/parser.py +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/hcl2/py.typed +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/mypy.ini +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/pylintrc +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/pyproject.toml +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/python_hcl2.egg-info/dependency_links.txt +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/python_hcl2.egg-info/entry_points.txt +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/python_hcl2.egg-info/not-zip-safe +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/python_hcl2.egg-info/requires.txt +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/python_hcl2.egg-info/top_level.txt +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/reports/.gitignore +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/requirements.txt +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/setup.cfg +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/test-requirements.txt +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/tox.ini +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.1.0}/tree-to-hcl2-reconstruction.md +0 -0
|
@@ -5,6 +5,21 @@ 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.1.0\] - 2025-04-10
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `hcl2.builder.Builder` - nested blocks support ([#214](https://github.com/amplify-education/python-hcl2/pull/214))
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Issue parsing parenthesesed identifier (reference) as an object key ([#212](https://github.com/amplify-education/python-hcl2/pull/212))
|
|
21
|
+
- Issue discarding empty lists when transforming python dictionary into Lark Tree ([#216](https://github.com/amplify-education/python-hcl2/pull/216))
|
|
22
|
+
|
|
8
23
|
## \[7.0.1\] - 2025-03-31
|
|
9
24
|
|
|
10
25
|
### Fixed
|
|
@@ -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
|
|
@@ -39,6 +39,7 @@ 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
|
|
@@ -74,7 +75,7 @@ EQ : /[ \t]*=(?!=|>)/
|
|
|
74
75
|
|
|
75
76
|
tuple : "[" (new_line_or_comment* expression new_line_or_comment* ",")* (new_line_or_comment* expression)? new_line_or_comment* "]"
|
|
76
77
|
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 |
|
|
78
|
+
object_elem : LPAR? object_elem_key RPAR? ( EQ | COLON ) expression
|
|
78
79
|
object_elem_key : float_lit | int_lit | identifier | STRING_LIT | object_elem_key_dot_accessor
|
|
79
80
|
object_elem_key_dot_accessor : identifier (DOT identifier)+
|
|
80
81
|
|
|
@@ -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
|
|
|
@@ -395,46 +397,41 @@ class HCLReverseTransformer:
|
|
|
395
397
|
|
|
396
398
|
return True
|
|
397
399
|
|
|
400
|
+
@classmethod
|
|
401
|
+
def _unwrap_interpolation(cls, value: str) -> str:
|
|
402
|
+
if cls._is_string_wrapped_tf(value):
|
|
403
|
+
return value[2:-1]
|
|
404
|
+
return value
|
|
405
|
+
|
|
398
406
|
def _newline(self, level: int, count: int = 1) -> Tree:
|
|
399
407
|
return Tree(
|
|
400
408
|
Token("RULE", "new_line_or_comment"),
|
|
401
409
|
[Token("NL_OR_COMMENT", f"\n{' ' * level}") for _ in range(count)],
|
|
402
410
|
)
|
|
403
411
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
|
412
|
+
def _is_block(self, value: Any) -> bool:
|
|
413
|
+
if isinstance(value, dict):
|
|
414
|
+
block_body = value
|
|
415
|
+
if (
|
|
416
|
+
START_LINE_KEY in block_body.keys()
|
|
417
|
+
or END_LINE_KEY in block_body.keys()
|
|
418
|
+
):
|
|
419
|
+
return True
|
|
417
420
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
421
|
+
try:
|
|
422
|
+
# if block is labeled, actual body might be nested
|
|
423
|
+
# pylint: disable=W0612
|
|
424
|
+
block_label, block_body = next(iter(value.items()))
|
|
425
|
+
except StopIteration:
|
|
426
|
+
# no more potential labels = nothing more to check
|
|
427
|
+
return False
|
|
422
428
|
|
|
423
|
-
|
|
424
|
-
# no metadata, it's just an array of objects, not a block
|
|
425
|
-
if len(list(sub_obj)) != 1:
|
|
426
|
-
return False
|
|
429
|
+
return self._is_block(block_body)
|
|
427
430
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
|
431
|
+
if isinstance(value, list):
|
|
432
|
+
if len(value) > 0:
|
|
433
|
+
return self._is_block(value[0])
|
|
435
434
|
|
|
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
435
|
return False
|
|
439
436
|
|
|
440
437
|
def _calculate_block_labels(self, block: dict) -> Tuple[List[str], dict]:
|
|
@@ -448,8 +445,8 @@ class HCLReverseTransformer:
|
|
|
448
445
|
|
|
449
446
|
# __start_line__ and __end_line__ metadata are not labels
|
|
450
447
|
if (
|
|
451
|
-
|
|
452
|
-
or
|
|
448
|
+
START_LINE_KEY in potential_body.keys()
|
|
449
|
+
or END_LINE_KEY in potential_body.keys()
|
|
453
450
|
):
|
|
454
451
|
return [curr_label], potential_body
|
|
455
452
|
|
|
@@ -457,6 +454,7 @@ class HCLReverseTransformer:
|
|
|
457
454
|
next_label, block_body = self._calculate_block_labels(potential_body)
|
|
458
455
|
return [curr_label] + next_label, block_body
|
|
459
456
|
|
|
457
|
+
# pylint:disable=R0914
|
|
460
458
|
def _transform_dict_to_body(self, hcl_dict: dict, level: int) -> Tree:
|
|
461
459
|
# we add a newline at the top of a body within a block, not the root body
|
|
462
460
|
# >2 here is to ignore the __start_line__ and __end_line__ metadata
|
|
@@ -467,14 +465,14 @@ class HCLReverseTransformer:
|
|
|
467
465
|
|
|
468
466
|
# iterate through each attribute or sub-block of this block
|
|
469
467
|
for key, value in hcl_dict.items():
|
|
470
|
-
if key in [
|
|
468
|
+
if key in [START_LINE_KEY, END_LINE_KEY]:
|
|
471
469
|
continue
|
|
472
470
|
|
|
473
471
|
# construct the identifier, whether that be a block type name or an attribute key
|
|
474
472
|
identifier_name = self._name_to_identifier(key)
|
|
475
473
|
|
|
476
474
|
# first, check whether the value is a "block"
|
|
477
|
-
if
|
|
475
|
+
if self._is_block(value):
|
|
478
476
|
for block_v in value:
|
|
479
477
|
block_labels, block_body_dict = self._calculate_block_labels(
|
|
480
478
|
block_v
|
|
@@ -493,7 +491,12 @@ class HCLReverseTransformer:
|
|
|
493
491
|
[identifier_name] + block_label_tokens + [block_body],
|
|
494
492
|
)
|
|
495
493
|
children.append(block)
|
|
496
|
-
|
|
494
|
+
# add empty line after block
|
|
495
|
+
new_line = self._newline(level - 1)
|
|
496
|
+
# add empty line with indentation for next element in the block
|
|
497
|
+
new_line.children.append(self._newline(level).children[0])
|
|
498
|
+
|
|
499
|
+
children.append(new_line)
|
|
497
500
|
|
|
498
501
|
# if the value isn't a block, it's an attribute
|
|
499
502
|
else:
|
|
@@ -556,10 +559,11 @@ class HCLReverseTransformer:
|
|
|
556
559
|
|
|
557
560
|
# iterate through the items and add them to the object
|
|
558
561
|
for i, (k, dict_v) in enumerate(value.items()):
|
|
559
|
-
if k in [
|
|
562
|
+
if k in [START_LINE_KEY, END_LINE_KEY]:
|
|
560
563
|
continue
|
|
561
564
|
|
|
562
565
|
value_expr_term = self._transform_value_to_expr_term(dict_v, level + 1)
|
|
566
|
+
k = self._unwrap_interpolation(k)
|
|
563
567
|
elements.append(
|
|
564
568
|
Tree(
|
|
565
569
|
Token("RULE", "object_elem"),
|
|
@@ -98,11 +98,14 @@ class DictTransformer(Transformer):
|
|
|
98
98
|
def object_elem(self, args: List) -> Dict:
|
|
99
99
|
# This returns a dict with a single key/value pair to make it easier to merge these
|
|
100
100
|
# into a bigger dict that is returned by the "object" function
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
if args[0] == Token("LPAR", "("):
|
|
102
|
+
key = self.strip_quotes(str(args[1].children[0]))
|
|
103
|
+
key = f"({key})"
|
|
104
|
+
key = self.to_string_dollar(key)
|
|
105
|
+
value = args[4]
|
|
104
106
|
else:
|
|
105
|
-
|
|
107
|
+
key = self.strip_quotes(str(args[0].children[0]))
|
|
108
|
+
value = args[2]
|
|
106
109
|
|
|
107
110
|
value = self.to_string_dollar(value)
|
|
108
111
|
return {key: value}
|
|
@@ -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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|