python-hcl2 7.2.0__tar.gz → 7.3.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.2.0 → python_hcl2-7.3.0}/CHANGELOG.md +14 -1
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/PKG-INFO +1 -1
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/hcl2/hcl2.lark +10 -9
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/hcl2/reconstructor.py +75 -16
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/hcl2/transformer.py +17 -9
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/hcl2/version.py +2 -2
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/python_hcl2.egg-info/PKG-INFO +1 -1
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/requirements.txt +1 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/.codacy.yml +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/.coveragerc +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/.github/CODEOWNERS +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/.github/ISSUE_TEMPLATE/hcl2-parsing-error.md +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/.github/workflows/codeql-analysis.yml +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/.github/workflows/pr_check.yml +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/.github/workflows/publish.yml +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/.gitignore +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/.pre-commit-config.yaml +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/.yamllint.yml +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/LICENSE +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/MANIFEST.in +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/README.md +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/bin/terraform_test +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/hcl2/__init__.py +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/hcl2/__main__.py +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/hcl2/api.py +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/hcl2/builder.py +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/hcl2/const.py +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/hcl2/parser.py +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/hcl2/py.typed +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/mypy.ini +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/pylintrc +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/pyproject.toml +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/python_hcl2.egg-info/SOURCES.txt +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/python_hcl2.egg-info/dependency_links.txt +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/python_hcl2.egg-info/entry_points.txt +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/python_hcl2.egg-info/not-zip-safe +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/python_hcl2.egg-info/requires.txt +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/python_hcl2.egg-info/top_level.txt +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/reports/.gitignore +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/setup.cfg +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/test-requirements.txt +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/tox.ini +0 -0
- {python_hcl2-7.2.0 → python_hcl2-7.3.0}/tree-to-hcl2-reconstruction.md +0 -0
|
@@ -7,7 +7,20 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|
|
7
7
|
|
|
8
8
|
## \[Unreleased\]
|
|
9
9
|
|
|
10
|
-
- Nothing
|
|
10
|
+
- Nothing yet.
|
|
11
|
+
|
|
12
|
+
## \[7.3.0\] - 2025-07-23
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Issue parsing interpolations and escaped interpolations in a single string. ([#239](https://github.com/amplify-education/python-hcl2/pull/239))
|
|
17
|
+
|
|
18
|
+
## \[7.2.1\] - 2025-05-16
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- More robust escaping for special characters. Thanks, @eranor ([#224](https://github.com/amplify-education/python-hcl2/pull/224))
|
|
23
|
+
- Issue parsing interpolation string as an object key ([#232](https://github.com/amplify-education/python-hcl2/pull/232))
|
|
11
24
|
|
|
12
25
|
## \[7.2.0\] - 2025-04-24
|
|
13
26
|
|
|
@@ -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 |
|
|
4
|
+
block : identifier (identifier | string)* 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
|
|
|
@@ -44,8 +44,7 @@ COLON : ":"
|
|
|
44
44
|
expr_term : LPAR new_line_or_comment? expression new_line_or_comment? RPAR
|
|
45
45
|
| float_lit
|
|
46
46
|
| int_lit
|
|
47
|
-
|
|
|
48
|
-
| string_with_interpolation
|
|
47
|
+
| string
|
|
49
48
|
| tuple
|
|
50
49
|
| object
|
|
51
50
|
| function_call
|
|
@@ -60,11 +59,13 @@ expr_term : LPAR new_line_or_comment? expression new_line_or_comment? RPAR
|
|
|
60
59
|
| for_tuple_expr
|
|
61
60
|
| for_object_expr
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
string: "\"" string_part* "\""
|
|
63
|
+
string_part: STRING_CHARS
|
|
64
|
+
| ESCAPED_INTERPOLATION
|
|
65
|
+
| interpolation
|
|
66
|
+
interpolation: "${" expression "}"
|
|
67
|
+
ESCAPED_INTERPOLATION.2: /\$\$\{[^}]*\}/
|
|
68
|
+
STRING_CHARS.1: /(?:(?!\$\$\{)(?!\$\{)[^"\\]|\\.|(?:\$(?!\$?\{)))+/
|
|
68
69
|
|
|
69
70
|
int_lit : NEGATIVE_DECIMAL? DECIMAL+ | NEGATIVE_DECIMAL+
|
|
70
71
|
!float_lit: (NEGATIVE_DECIMAL? DECIMAL+ | NEGATIVE_DECIMAL+) "." DECIMAL+ (EXP_MARK)?
|
|
@@ -77,7 +78,7 @@ EQ : /[ \t]*=(?!=|>)/
|
|
|
77
78
|
tuple : "[" (new_line_or_comment* expression new_line_or_comment* ",")* (new_line_or_comment* expression)? new_line_or_comment* "]"
|
|
78
79
|
object : "{" new_line_or_comment? (new_line_or_comment* (object_elem | (object_elem COMMA)) new_line_or_comment*)* "}"
|
|
79
80
|
object_elem : object_elem_key ( EQ | COLON ) expression
|
|
80
|
-
object_elem_key : float_lit | int_lit | identifier |
|
|
81
|
+
object_elem_key : float_lit | int_lit | identifier | string | object_elem_key_dot_accessor | object_elem_key_expression
|
|
81
82
|
object_elem_key_expression : LPAR expression RPAR
|
|
82
83
|
object_elem_key_dot_accessor : identifier (DOT identifier)+
|
|
83
84
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""A reconstructor for HCL2 implemented using Lark's experimental reconstruction functionality"""
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
|
-
import json
|
|
5
4
|
from typing import List, Dict, Callable, Optional, Union, Any, Tuple
|
|
6
5
|
|
|
7
6
|
from lark import Lark, Tree
|
|
@@ -10,6 +9,7 @@ from lark.lexer import Token, PatternStr, TerminalDef
|
|
|
10
9
|
from lark.reconstruct import Reconstructor
|
|
11
10
|
from lark.tree_matcher import is_discarded_terminal
|
|
12
11
|
from lark.visitors import Transformer_InPlace
|
|
12
|
+
from regex import regex
|
|
13
13
|
|
|
14
14
|
from hcl2.const import START_LINE_KEY, END_LINE_KEY
|
|
15
15
|
from hcl2.parser import reconstruction_parser
|
|
@@ -137,7 +137,7 @@ class HCLReconstructor(Reconstructor):
|
|
|
137
137
|
)
|
|
138
138
|
|
|
139
139
|
# pylint: disable=too-many-branches, too-many-return-statements
|
|
140
|
-
def _should_add_space(self, rule, current_terminal):
|
|
140
|
+
def _should_add_space(self, rule, current_terminal, is_block_label: bool = False):
|
|
141
141
|
"""
|
|
142
142
|
This method documents the situations in which we add space around
|
|
143
143
|
certain tokens while reconstructing the generated HCL.
|
|
@@ -155,6 +155,7 @@ class HCLReconstructor(Reconstructor):
|
|
|
155
155
|
|
|
156
156
|
This should be sufficient to make a spacing decision.
|
|
157
157
|
"""
|
|
158
|
+
|
|
158
159
|
# we don't need to add multiple spaces
|
|
159
160
|
if self._last_char_space:
|
|
160
161
|
return False
|
|
@@ -166,6 +167,14 @@ class HCLReconstructor(Reconstructor):
|
|
|
166
167
|
if self._is_equals_sign(current_terminal):
|
|
167
168
|
return True
|
|
168
169
|
|
|
170
|
+
if is_block_label and isinstance(rule, Token) and rule.value == "string":
|
|
171
|
+
if (
|
|
172
|
+
current_terminal == self._last_terminal == Terminal("DBLQUOTE")
|
|
173
|
+
or current_terminal == Terminal("DBLQUOTE")
|
|
174
|
+
and self._last_terminal == Terminal("NAME")
|
|
175
|
+
):
|
|
176
|
+
return True
|
|
177
|
+
|
|
169
178
|
# if we're in a ternary or binary operator, add space around the operator
|
|
170
179
|
if (
|
|
171
180
|
isinstance(rule, Token)
|
|
@@ -235,7 +244,7 @@ class HCLReconstructor(Reconstructor):
|
|
|
235
244
|
return True
|
|
236
245
|
|
|
237
246
|
# always add space between string literals
|
|
238
|
-
if current_terminal == Terminal("
|
|
247
|
+
if current_terminal == Terminal("STRING_CHARS"):
|
|
239
248
|
return True
|
|
240
249
|
|
|
241
250
|
# if we just opened a block, add a space, unless the block is empty
|
|
@@ -257,7 +266,7 @@ class HCLReconstructor(Reconstructor):
|
|
|
257
266
|
# preceded by a space if they're following a comma in a tuple or
|
|
258
267
|
# function arg
|
|
259
268
|
if current_terminal in [
|
|
260
|
-
Terminal("
|
|
269
|
+
Terminal("DBLQUOTE"),
|
|
261
270
|
Terminal("DECIMAL"),
|
|
262
271
|
Terminal("NAME"),
|
|
263
272
|
Terminal("NEGATIVE_DECIMAL"),
|
|
@@ -267,13 +276,15 @@ class HCLReconstructor(Reconstructor):
|
|
|
267
276
|
# the catch-all case, we're not sure, so don't add a space
|
|
268
277
|
return False
|
|
269
278
|
|
|
270
|
-
def _reconstruct(self, tree):
|
|
279
|
+
def _reconstruct(self, tree, is_block_label=False):
|
|
271
280
|
unreduced_tree = self.match_tree(tree, tree.data)
|
|
272
281
|
res = self.write_tokens.transform(unreduced_tree)
|
|
273
282
|
for item in res:
|
|
274
283
|
# any time we encounter a child tree, we recurse
|
|
275
284
|
if isinstance(item, Tree):
|
|
276
|
-
yield from self._reconstruct(
|
|
285
|
+
yield from self._reconstruct(
|
|
286
|
+
item, (unreduced_tree.data == "block" and item.data != "body")
|
|
287
|
+
)
|
|
277
288
|
|
|
278
289
|
# every leaf should be a tuple, which contains information about
|
|
279
290
|
# which terminal the leaf represents
|
|
@@ -309,7 +320,7 @@ class HCLReconstructor(Reconstructor):
|
|
|
309
320
|
self._deferred_item = None
|
|
310
321
|
|
|
311
322
|
# potentially add a space before the next token
|
|
312
|
-
if self._should_add_space(rule, terminal):
|
|
323
|
+
if self._should_add_space(rule, terminal, is_block_label):
|
|
313
324
|
yield " "
|
|
314
325
|
self._last_char_space = True
|
|
315
326
|
|
|
@@ -353,11 +364,21 @@ class HCLReverseTransformer:
|
|
|
353
364
|
|
|
354
365
|
@staticmethod
|
|
355
366
|
def _escape_interpolated_str(interp_s: str) -> str:
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
367
|
+
if interp_s.strip().startswith("<<-") or interp_s.strip().startswith("<<"):
|
|
368
|
+
# For heredoc strings, preserve their format exactly
|
|
369
|
+
return reverse_quotes_within_interpolation(interp_s)
|
|
370
|
+
# Escape backslashes first (very important to do this first)
|
|
371
|
+
escaped = interp_s.replace("\\", "\\\\")
|
|
372
|
+
# Escape quotes
|
|
373
|
+
escaped = escaped.replace('"', '\\"')
|
|
374
|
+
# Escape control characters
|
|
375
|
+
escaped = escaped.replace("\n", "\\n")
|
|
376
|
+
escaped = escaped.replace("\r", "\\r")
|
|
377
|
+
escaped = escaped.replace("\t", "\\t")
|
|
378
|
+
escaped = escaped.replace("\b", "\\b")
|
|
379
|
+
escaped = escaped.replace("\f", "\\f")
|
|
359
380
|
# find each interpolation within the string and remove the backslashes
|
|
360
|
-
interp_s = reverse_quotes_within_interpolation(
|
|
381
|
+
interp_s = reverse_quotes_within_interpolation(f"{escaped}")
|
|
361
382
|
return interp_s
|
|
362
383
|
|
|
363
384
|
@staticmethod
|
|
@@ -410,6 +431,44 @@ class HCLReverseTransformer:
|
|
|
410
431
|
[Token("NL_OR_COMMENT", f"\n{' ' * level}") for _ in range(count)],
|
|
411
432
|
)
|
|
412
433
|
|
|
434
|
+
def _build_string_rule(self, string: str, level: int = 0) -> Tree:
|
|
435
|
+
# grammar in hcl2.lark defines that a string is built of any number of string parts,
|
|
436
|
+
# each string part can be either interpolation expression, escaped interpolation string
|
|
437
|
+
# or regular string
|
|
438
|
+
# this method build hcl2 string rule based on arbitrary string,
|
|
439
|
+
# splitting such string into individual parts and building a lark tree out of them
|
|
440
|
+
#
|
|
441
|
+
result = []
|
|
442
|
+
|
|
443
|
+
pattern = regex.compile(r"(\${1,2}\{(?:[^{}]|(?R))*\})")
|
|
444
|
+
parts = [part for part in pattern.split(string) if part != ""]
|
|
445
|
+
# e.g. 'aaa$${bbb}ccc${"ddd-${eee}"}' -> ['aaa', '$${bbb}', 'ccc', '${"ddd-${eee}"}']
|
|
446
|
+
# 'aa-${"bb-${"cc-${"dd-${5 + 5}"}"}"}' -> ['aa-', '${"bb-${"cc-${"dd-${5 + 5}"}"}"}']
|
|
447
|
+
|
|
448
|
+
for part in parts:
|
|
449
|
+
if part.startswith("$${") and part.endswith("}"):
|
|
450
|
+
result.append(Token("ESCAPED_INTERPOLATION", part))
|
|
451
|
+
|
|
452
|
+
# unwrap interpolation expression and recurse into it
|
|
453
|
+
elif part.startswith("${") and part.endswith("}"):
|
|
454
|
+
part = part[2:-1]
|
|
455
|
+
if part.startswith('"') and part.endswith('"'):
|
|
456
|
+
part = part[1:-1]
|
|
457
|
+
part = self._transform_value_to_expr_term(part, level)
|
|
458
|
+
else:
|
|
459
|
+
part = Tree(
|
|
460
|
+
Token("RULE", "expr_term"),
|
|
461
|
+
[Tree(Token("RULE", "identifier"), [Token("NAME", part)])],
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
result.append(Tree(Token("RULE", "interpolation"), [part]))
|
|
465
|
+
|
|
466
|
+
else:
|
|
467
|
+
result.append(Token("STRING_CHARS", part))
|
|
468
|
+
|
|
469
|
+
result = [Tree(Token("RULE", "string_part"), [element]) for element in result]
|
|
470
|
+
return Tree(Token("RULE", "string"), result)
|
|
471
|
+
|
|
413
472
|
def _is_block(self, value: Any) -> bool:
|
|
414
473
|
if isinstance(value, dict):
|
|
415
474
|
block_body = value
|
|
@@ -475,8 +534,8 @@ class HCLReverseTransformer:
|
|
|
475
534
|
block_labels, block_body_dict = self._calculate_block_labels(
|
|
476
535
|
block_v
|
|
477
536
|
)
|
|
478
|
-
|
|
479
|
-
|
|
537
|
+
block_label_trees = [
|
|
538
|
+
self._build_string_rule(block_label, level)
|
|
480
539
|
for block_label in block_labels
|
|
481
540
|
]
|
|
482
541
|
block_body = self._transform_dict_to_body(
|
|
@@ -486,7 +545,7 @@ class HCLReverseTransformer:
|
|
|
486
545
|
# create our actual block to add to our own body
|
|
487
546
|
block = Tree(
|
|
488
547
|
Token("RULE", "block"),
|
|
489
|
-
[identifier_name] +
|
|
548
|
+
[identifier_name] + block_label_trees + [block_body],
|
|
490
549
|
)
|
|
491
550
|
children.append(block)
|
|
492
551
|
# add empty line after block
|
|
@@ -665,10 +724,10 @@ class HCLReverseTransformer:
|
|
|
665
724
|
parsed_value = attribute.children[2]
|
|
666
725
|
return parsed_value
|
|
667
726
|
|
|
668
|
-
# otherwise it's
|
|
727
|
+
# otherwise it's a string
|
|
669
728
|
return Tree(
|
|
670
729
|
Token("RULE", "expr_term"),
|
|
671
|
-
[
|
|
730
|
+
[self._build_string_rule(self._escape_interpolated_str(value), level)],
|
|
672
731
|
)
|
|
673
732
|
|
|
674
733
|
# otherwise, we don't know the type
|
|
@@ -102,10 +102,12 @@ class DictTransformer(Transformer):
|
|
|
102
102
|
# This returns a dict with a single key/value pair to make it easier to merge these
|
|
103
103
|
# into a bigger dict that is returned by the "object" function
|
|
104
104
|
|
|
105
|
-
key =
|
|
106
|
-
|
|
105
|
+
key = str(args[0].children[0])
|
|
106
|
+
if not re.match(r".*?(\${).*}.*", key):
|
|
107
|
+
# do not strip quotes of a interpolation string
|
|
108
|
+
key = self.strip_quotes(key)
|
|
107
109
|
|
|
108
|
-
value = self.to_string_dollar(
|
|
110
|
+
value = self.to_string_dollar(args[2])
|
|
109
111
|
return {key: value}
|
|
110
112
|
|
|
111
113
|
def object_elem_key_dot_accessor(self, args: List) -> str:
|
|
@@ -245,7 +247,8 @@ class DictTransformer(Transformer):
|
|
|
245
247
|
raise RuntimeError(f"Invalid Heredoc token: {args[0]}")
|
|
246
248
|
|
|
247
249
|
trim_chars = "\n\t "
|
|
248
|
-
|
|
250
|
+
result = match.group(2).rstrip(trim_chars)
|
|
251
|
+
return f'"{result}"'
|
|
249
252
|
|
|
250
253
|
def heredoc_template_trim(self, args: List) -> str:
|
|
251
254
|
# See https://github.com/hashicorp/hcl2/blob/master/hcl/hclsyntax/spec.md#template-expressions
|
|
@@ -295,12 +298,17 @@ class DictTransformer(Transformer):
|
|
|
295
298
|
# e.g. f"{2 + 2} {{2 + 2}}" == "4 {2 + 2}"
|
|
296
299
|
return f"{{{for_expr}}}"
|
|
297
300
|
|
|
298
|
-
def
|
|
299
|
-
return '"' +
|
|
301
|
+
def string(self, args: List) -> str:
|
|
302
|
+
return '"' + "".join(args) + '"'
|
|
303
|
+
|
|
304
|
+
def string_part(self, args: List) -> str:
|
|
305
|
+
value = self.to_tf_inline(args[0])
|
|
306
|
+
if value.startswith('"') and value.endswith('"'):
|
|
307
|
+
value = value[1:-1]
|
|
308
|
+
return value
|
|
300
309
|
|
|
301
|
-
def
|
|
302
|
-
|
|
303
|
-
return "${" + ("".join(args)) + "}"
|
|
310
|
+
def interpolation(self, args: List) -> str:
|
|
311
|
+
return '"${' + str(args[0]) + '}"'
|
|
304
312
|
|
|
305
313
|
def strip_new_line_tokens(self, args: List) -> List:
|
|
306
314
|
"""
|
|
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
|
|
File without changes
|
|
File without changes
|