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.
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/CHANGELOG.md +27 -0
- {python_hcl2-7.0.1/python_hcl2.egg-info → python_hcl2-7.2.0}/PKG-INFO +8 -13
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/README.md +7 -12
- python_hcl2-7.2.0/hcl2/builder.py +86 -0
- python_hcl2-7.2.0/hcl2/const.py +4 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/hcl2/hcl2.lark +11 -8
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/hcl2/reconstructor.py +70 -37
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/hcl2/transformer.py +31 -15
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/hcl2/version.py +2 -2
- {python_hcl2-7.0.1 → python_hcl2-7.2.0/python_hcl2.egg-info}/PKG-INFO +8 -13
- {python_hcl2-7.0.1 → python_hcl2-7.2.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.2.0}/.codacy.yml +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/.coveragerc +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/.github/CODEOWNERS +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/.github/ISSUE_TEMPLATE/hcl2-parsing-error.md +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/.github/workflows/codeql-analysis.yml +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/.github/workflows/pr_check.yml +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/.github/workflows/publish.yml +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/.gitignore +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/.pre-commit-config.yaml +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/.yamllint.yml +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/LICENSE +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/MANIFEST.in +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/bin/terraform_test +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/hcl2/__init__.py +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/hcl2/__main__.py +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/hcl2/api.py +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/hcl2/parser.py +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/hcl2/py.typed +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/mypy.ini +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/pylintrc +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/pyproject.toml +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/python_hcl2.egg-info/dependency_links.txt +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/python_hcl2.egg-info/entry_points.txt +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/python_hcl2.egg-info/not-zip-safe +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/python_hcl2.egg-info/requires.txt +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/python_hcl2.egg-info/top_level.txt +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/reports/.gitignore +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/requirements.txt +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/setup.cfg +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/test-requirements.txt +0 -0
- {python_hcl2-7.0.1 → python_hcl2-7.2.0}/tox.ini +0 -0
- {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
|
|
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
|
-
###
|
|
117
|
+
### Using inline expression as an object key
|
|
118
118
|
|
|
119
|
-
-
|
|
119
|
+
- Object key can be an expression as long as it is wrapped in parentheses:
|
|
120
120
|
```terraform
|
|
121
121
|
locals {
|
|
122
|
-
foo = "
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
###
|
|
90
|
+
### Using inline expression as an object key
|
|
91
91
|
|
|
92
|
-
-
|
|
92
|
+
- Object key can be an expression as long as it is wrapped in parentheses:
|
|
93
93
|
```terraform
|
|
94
94
|
locals {
|
|
95
|
-
foo = "
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
@@ -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 : "\""
|
|
62
|
-
STRING_CHARS : /(?:(?!\${)([^"\\]
|
|
63
|
-
|
|
64
|
-
|
|
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 |
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
452
|
-
or
|
|
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 [
|
|
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
|
|
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
|
-
|
|
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 [
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
311
|
-
|
|
322
|
+
if self.is_string_dollar(value):
|
|
323
|
+
return value
|
|
312
324
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
318
|
-
return value
|
|
332
|
+
return f"${{{value}}}"
|
|
319
333
|
|
|
320
|
-
|
|
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:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-hcl2
|
|
3
|
-
Version: 7.0
|
|
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
|
-
###
|
|
117
|
+
### Using inline expression as an object key
|
|
118
118
|
|
|
119
|
-
-
|
|
119
|
+
- Object key can be an expression as long as it is wrapped in parentheses:
|
|
120
120
|
```terraform
|
|
121
121
|
locals {
|
|
122
|
-
foo = "
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
```
|
|
@@ -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
|