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.
Files changed (43) hide show
  1. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/CHANGELOG.md +14 -1
  2. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/PKG-INFO +1 -1
  3. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/hcl2/hcl2.lark +10 -9
  4. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/hcl2/reconstructor.py +75 -16
  5. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/hcl2/transformer.py +17 -9
  6. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/hcl2/version.py +2 -2
  7. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/python_hcl2.egg-info/PKG-INFO +1 -1
  8. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/requirements.txt +1 -0
  9. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/.codacy.yml +0 -0
  10. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/.coveragerc +0 -0
  11. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/.github/CODEOWNERS +0 -0
  12. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/.github/ISSUE_TEMPLATE/hcl2-parsing-error.md +0 -0
  13. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/.github/workflows/codeql-analysis.yml +0 -0
  14. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/.github/workflows/pr_check.yml +0 -0
  15. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/.github/workflows/publish.yml +0 -0
  16. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/.gitignore +0 -0
  17. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/.pre-commit-config.yaml +0 -0
  18. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/.yamllint.yml +0 -0
  19. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/LICENSE +0 -0
  20. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/MANIFEST.in +0 -0
  21. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/README.md +0 -0
  22. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/bin/terraform_test +0 -0
  23. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/hcl2/__init__.py +0 -0
  24. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/hcl2/__main__.py +0 -0
  25. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/hcl2/api.py +0 -0
  26. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/hcl2/builder.py +0 -0
  27. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/hcl2/const.py +0 -0
  28. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/hcl2/parser.py +0 -0
  29. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/hcl2/py.typed +0 -0
  30. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/mypy.ini +0 -0
  31. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/pylintrc +0 -0
  32. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/pyproject.toml +0 -0
  33. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/python_hcl2.egg-info/SOURCES.txt +0 -0
  34. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/python_hcl2.egg-info/dependency_links.txt +0 -0
  35. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/python_hcl2.egg-info/entry_points.txt +0 -0
  36. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/python_hcl2.egg-info/not-zip-safe +0 -0
  37. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/python_hcl2.egg-info/requires.txt +0 -0
  38. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/python_hcl2.egg-info/top_level.txt +0 -0
  39. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/reports/.gitignore +0 -0
  40. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/setup.cfg +0 -0
  41. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/test-requirements.txt +0 -0
  42. {python_hcl2-7.2.0 → python_hcl2-7.3.0}/tox.ini +0 -0
  43. {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 Yet
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-hcl2
3
- Version: 7.2.0
3
+ Version: 7.3.0
4
4
  Summary: A parser for HCL2
5
5
  Author-email: Amplify Education <github@amplify.com>
6
6
  License: MIT
@@ -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 | string_with_interpolation)* new_line_or_comment? "{" body "}"
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
- | STRING_LIT
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
- 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 "}"
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 | STRING_LIT | object_elem_key_dot_accessor | object_elem_key_expression
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("STRING_LIT"):
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("STRING_LIT"),
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(item)
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
- # begin by doing basic JSON string escaping, to add backslashes
357
- interp_s = json.dumps(interp_s)
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(interp_s)
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
- block_label_tokens = [
479
- Token("STRING_LIT", f'"{block_label}"')
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] + block_label_tokens + [block_body],
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 just a string.
727
+ # otherwise it's a string
669
728
  return Tree(
670
729
  Token("RULE", "expr_term"),
671
- [Token("STRING_LIT", self._escape_interpolated_str(value))],
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 = self.strip_quotes(str(args[0].children[0]))
106
- value = args[2]
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(value)
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
- return f'"{match.group(2).rstrip(trim_chars)}"'
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 string_with_interpolation(self, args: List) -> str:
299
- return '"' + ("".join(args)) + '"'
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 interpolation_maybe_nested(self, args: List) -> str:
302
- # return "".join(args)
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
  """
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '7.2.0'
21
- __version_tuple__ = version_tuple = (7, 2, 0)
20
+ __version__ = version = '7.3.0'
21
+ __version_tuple__ = version_tuple = (7, 3, 0)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-hcl2
3
- Version: 7.2.0
3
+ Version: 7.3.0
4
4
  Summary: A parser for HCL2
5
5
  Author-email: Amplify Education <github@amplify.com>
6
6
  License: MIT
@@ -1,3 +1,4 @@
1
1
  # Place dependencies in this file, following the distutils format:
2
2
  # http://docs.python.org/2/distutils/setupscript.html#relationships-between-distributions-and-packages
3
3
  lark>=1.1.5,<2.0
4
+ regex>=2024.4.16
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