python-hcl2 7.2.1__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.1 → python_hcl2-7.3.0}/CHANGELOG.md +6 -0
  2. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/PKG-INFO +1 -1
  3. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/hcl2/hcl2.lark +10 -9
  4. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/hcl2/reconstructor.py +69 -20
  5. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/hcl2/transformer.py +12 -6
  6. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/hcl2/version.py +2 -2
  7. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/python_hcl2.egg-info/PKG-INFO +1 -1
  8. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/requirements.txt +1 -0
  9. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/.codacy.yml +0 -0
  10. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/.coveragerc +0 -0
  11. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/.github/CODEOWNERS +0 -0
  12. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/.github/ISSUE_TEMPLATE/hcl2-parsing-error.md +0 -0
  13. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/.github/workflows/codeql-analysis.yml +0 -0
  14. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/.github/workflows/pr_check.yml +0 -0
  15. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/.github/workflows/publish.yml +0 -0
  16. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/.gitignore +0 -0
  17. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/.pre-commit-config.yaml +0 -0
  18. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/.yamllint.yml +0 -0
  19. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/LICENSE +0 -0
  20. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/MANIFEST.in +0 -0
  21. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/README.md +0 -0
  22. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/bin/terraform_test +0 -0
  23. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/hcl2/__init__.py +0 -0
  24. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/hcl2/__main__.py +0 -0
  25. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/hcl2/api.py +0 -0
  26. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/hcl2/builder.py +0 -0
  27. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/hcl2/const.py +0 -0
  28. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/hcl2/parser.py +0 -0
  29. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/hcl2/py.typed +0 -0
  30. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/mypy.ini +0 -0
  31. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/pylintrc +0 -0
  32. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/pyproject.toml +0 -0
  33. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/python_hcl2.egg-info/SOURCES.txt +0 -0
  34. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/python_hcl2.egg-info/dependency_links.txt +0 -0
  35. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/python_hcl2.egg-info/entry_points.txt +0 -0
  36. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/python_hcl2.egg-info/not-zip-safe +0 -0
  37. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/python_hcl2.egg-info/requires.txt +0 -0
  38. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/python_hcl2.egg-info/top_level.txt +0 -0
  39. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/reports/.gitignore +0 -0
  40. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/setup.cfg +0 -0
  41. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/test-requirements.txt +0 -0
  42. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/tox.ini +0 -0
  43. {python_hcl2-7.2.1 → python_hcl2-7.3.0}/tree-to-hcl2-reconstruction.md +0 -0
@@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
9
9
 
10
10
  - Nothing yet.
11
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
+
12
18
  ## \[7.2.1\] - 2025-05-16
13
19
 
14
20
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-hcl2
3
- Version: 7.2.1
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 | string_with_interpolation
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,21 +364,21 @@ class HCLReverseTransformer:
353
364
 
354
365
  @staticmethod
355
366
  def _escape_interpolated_str(interp_s: str) -> str:
356
- if interp_s.strip().startswith('<<-') or interp_s.strip().startswith('<<'):
367
+ if interp_s.strip().startswith("<<-") or interp_s.strip().startswith("<<"):
357
368
  # For heredoc strings, preserve their format exactly
358
369
  return reverse_quotes_within_interpolation(interp_s)
359
370
  # Escape backslashes first (very important to do this first)
360
- escaped = interp_s.replace('\\', '\\\\')
371
+ escaped = interp_s.replace("\\", "\\\\")
361
372
  # Escape quotes
362
373
  escaped = escaped.replace('"', '\\"')
363
374
  # Escape control characters
364
- escaped = escaped.replace('\n', '\\n')
365
- escaped = escaped.replace('\r', '\\r')
366
- escaped = escaped.replace('\t', '\\t')
367
- escaped = escaped.replace('\b', '\\b')
368
- escaped = escaped.replace('\f', '\\f')
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")
369
380
  # find each interpolation within the string and remove the backslashes
370
- interp_s = reverse_quotes_within_interpolation(f'"{escaped}"')
381
+ interp_s = reverse_quotes_within_interpolation(f"{escaped}")
371
382
  return interp_s
372
383
 
373
384
  @staticmethod
@@ -420,6 +431,44 @@ class HCLReverseTransformer:
420
431
  [Token("NL_OR_COMMENT", f"\n{' ' * level}") for _ in range(count)],
421
432
  )
422
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
+
423
472
  def _is_block(self, value: Any) -> bool:
424
473
  if isinstance(value, dict):
425
474
  block_body = value
@@ -485,8 +534,8 @@ class HCLReverseTransformer:
485
534
  block_labels, block_body_dict = self._calculate_block_labels(
486
535
  block_v
487
536
  )
488
- block_label_tokens = [
489
- Token("STRING_LIT", f'"{block_label}"')
537
+ block_label_trees = [
538
+ self._build_string_rule(block_label, level)
490
539
  for block_label in block_labels
491
540
  ]
492
541
  block_body = self._transform_dict_to_body(
@@ -496,7 +545,7 @@ class HCLReverseTransformer:
496
545
  # create our actual block to add to our own body
497
546
  block = Tree(
498
547
  Token("RULE", "block"),
499
- [identifier_name] + block_label_tokens + [block_body],
548
+ [identifier_name] + block_label_trees + [block_body],
500
549
  )
501
550
  children.append(block)
502
551
  # add empty line after block
@@ -675,10 +724,10 @@ class HCLReverseTransformer:
675
724
  parsed_value = attribute.children[2]
676
725
  return parsed_value
677
726
 
678
- # otherwise it's just a string.
727
+ # otherwise it's a string
679
728
  return Tree(
680
729
  Token("RULE", "expr_term"),
681
- [Token("STRING_LIT", self._escape_interpolated_str(value))],
730
+ [self._build_string_rule(self._escape_interpolated_str(value), level)],
682
731
  )
683
732
 
684
733
  # otherwise, we don't know the type
@@ -247,7 +247,8 @@ class DictTransformer(Transformer):
247
247
  raise RuntimeError(f"Invalid Heredoc token: {args[0]}")
248
248
 
249
249
  trim_chars = "\n\t "
250
- return f'"{match.group(2).rstrip(trim_chars)}"'
250
+ result = match.group(2).rstrip(trim_chars)
251
+ return f'"{result}"'
251
252
 
252
253
  def heredoc_template_trim(self, args: List) -> str:
253
254
  # See https://github.com/hashicorp/hcl2/blob/master/hcl/hclsyntax/spec.md#template-expressions
@@ -297,12 +298,17 @@ class DictTransformer(Transformer):
297
298
  # e.g. f"{2 + 2} {{2 + 2}}" == "4 {2 + 2}"
298
299
  return f"{{{for_expr}}}"
299
300
 
300
- def string_with_interpolation(self, args: List) -> str:
301
- return '"' + ("".join(args)) + '"'
301
+ def string(self, args: List) -> str:
302
+ return '"' + "".join(args) + '"'
302
303
 
303
- def interpolation_maybe_nested(self, args: List) -> str:
304
- # return "".join(args)
305
- return "${" + ("".join(args)) + "}"
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
309
+
310
+ def interpolation(self, args: List) -> str:
311
+ return '"${' + str(args[0]) + '}"'
306
312
 
307
313
  def strip_new_line_tokens(self, args: List) -> List:
308
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.1'
21
- __version_tuple__ = version_tuple = (7, 2, 1)
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.1
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