python-hcl2 7.1.0__tar.gz → 7.2.1__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.1.0 → python_hcl2-7.2.1}/CHANGELOG.md +20 -1
  2. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/PKG-INFO +8 -13
  3. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/README.md +7 -12
  4. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/hcl2/hcl2.lark +10 -8
  5. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/hcl2/reconstructor.py +48 -9
  6. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/hcl2/transformer.py +36 -21
  7. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/hcl2/version.py +2 -2
  8. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/python_hcl2.egg-info/PKG-INFO +8 -13
  9. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/.codacy.yml +0 -0
  10. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/.coveragerc +0 -0
  11. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/.github/CODEOWNERS +0 -0
  12. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/.github/ISSUE_TEMPLATE/hcl2-parsing-error.md +0 -0
  13. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/.github/workflows/codeql-analysis.yml +0 -0
  14. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/.github/workflows/pr_check.yml +0 -0
  15. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/.github/workflows/publish.yml +0 -0
  16. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/.gitignore +0 -0
  17. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/.pre-commit-config.yaml +0 -0
  18. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/.yamllint.yml +0 -0
  19. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/LICENSE +0 -0
  20. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/MANIFEST.in +0 -0
  21. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/bin/terraform_test +0 -0
  22. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/hcl2/__init__.py +0 -0
  23. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/hcl2/__main__.py +0 -0
  24. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/hcl2/api.py +0 -0
  25. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/hcl2/builder.py +0 -0
  26. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/hcl2/const.py +0 -0
  27. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/hcl2/parser.py +0 -0
  28. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/hcl2/py.typed +0 -0
  29. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/mypy.ini +0 -0
  30. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/pylintrc +0 -0
  31. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/pyproject.toml +0 -0
  32. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/python_hcl2.egg-info/SOURCES.txt +0 -0
  33. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/python_hcl2.egg-info/dependency_links.txt +0 -0
  34. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/python_hcl2.egg-info/entry_points.txt +0 -0
  35. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/python_hcl2.egg-info/not-zip-safe +0 -0
  36. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/python_hcl2.egg-info/requires.txt +0 -0
  37. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/python_hcl2.egg-info/top_level.txt +0 -0
  38. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/reports/.gitignore +0 -0
  39. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/requirements.txt +0 -0
  40. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/setup.cfg +0 -0
  41. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/test-requirements.txt +0 -0
  42. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/tox.ini +0 -0
  43. {python_hcl2-7.1.0 → python_hcl2-7.2.1}/tree-to-hcl2-reconstruction.md +0 -0
@@ -7,7 +7,26 @@ 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.2.1\] - 2025-05-16
13
+
14
+ ### Fixed
15
+
16
+ - More robust escaping for special characters. Thanks, @eranor ([#224](https://github.com/amplify-education/python-hcl2/pull/224))
17
+ - Issue parsing interpolation string as an object key ([#232](https://github.com/amplify-education/python-hcl2/pull/232))
18
+
19
+ ## \[7.2.0\] - 2025-04-24
20
+
21
+ ### Added
22
+
23
+ - Possibility to parse deeply nested interpolations (formerly a Limitation), Thanks again, @weaversam8 ([#223](https://github.com/amplify-education/python-hcl2/pull/223))
24
+
25
+ ### Fixed
26
+
27
+ - Issue parsing ellipsis in a separate line within `for` expression ([#221](https://github.com/amplify-education/python-hcl2/pull/221))
28
+ - Issue parsing inline expression as an object key; **see Limitations in README.md** ([#222](https://github.com/amplify-education/python-hcl2/pull/222))
29
+ - Preserve literals of e-notation floats in parsing and reconstruction. Thanks, @eranor ([#226](https://github.com/amplify-education/python-hcl2/pull/226))
11
30
 
12
31
  ## \[7.1.0\] - 2025-04-10
13
32
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-hcl2
3
- Version: 7.1.0
3
+ Version: 7.2.1
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
- ### Error parsing string interpolations nested more than 2 times
117
+ ### Using inline expression as an object key
118
118
 
119
- - Parsing following example is expected to throw out an exception and fail:
119
+ - Object key can be an expression as long as it is wrapped in parentheses:
120
120
  ```terraform
121
121
  locals {
122
- foo = "foo"
123
- name = "prefix1-${"prefix2-${"${local.foo}-bar"}"}" //should interpolate into "prefix1-prefix2-foo-bar" but fails
124
- }
125
- ```
126
- We recommend working around this by modifying the configuration in the following manner:
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
- ### Error parsing string interpolations nested more than 2 times
90
+ ### Using inline expression as an object key
91
91
 
92
- - Parsing following example is expected to throw out an exception and fail:
92
+ - Object key can be an expression as long as it is wrapped in parentheses:
93
93
  ```terraform
94
94
  locals {
95
- foo = "foo"
96
- name = "prefix1-${"prefix2-${"${local.foo}-bar"}"}" //should interpolate into "prefix1-prefix2-foo-bar" but fails
97
- }
98
- ```
99
- We recommend working around this by modifying the configuration in the following manner:
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
  ```
@@ -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
 
@@ -45,6 +45,7 @@ expr_term : LPAR new_line_or_comment? expression new_line_or_comment? RPAR
45
45
  | float_lit
46
46
  | int_lit
47
47
  | STRING_LIT
48
+ | string_with_interpolation
48
49
  | tuple
49
50
  | object
50
51
  | function_call
@@ -59,10 +60,10 @@ expr_term : LPAR new_line_or_comment? expression new_line_or_comment? RPAR
59
60
  | for_tuple_expr
60
61
  | for_object_expr
61
62
 
62
- STRING_LIT : "\"" (STRING_CHARS | INTERPOLATION)* "\""
63
- STRING_CHARS : /(?:(?!\${)([^"\\]|\\.))+/+ // any character except '"" unless inside a interpolation string
64
- NESTED_INTERPOLATION : "${" /[^}]+/ "}"
65
- INTERPOLATION : "${" (/(?:(?!\${)([^}]))+/ | NESTED_INTERPOLATION)+ "}"
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 "}"
66
67
 
67
68
 
68
69
  int_lit : NEGATIVE_DECIMAL? DECIMAL+ | NEGATIVE_DECIMAL+
@@ -75,8 +76,9 @@ EQ : /[ \t]*=(?!=|>)/
75
76
 
76
77
  tuple : "[" (new_line_or_comment* expression new_line_or_comment* ",")* (new_line_or_comment* expression)? new_line_or_comment* "]"
77
78
  object : "{" new_line_or_comment? (new_line_or_comment* (object_elem | (object_elem COMMA)) new_line_or_comment*)* "}"
78
- object_elem : LPAR? object_elem_key RPAR? ( EQ | COLON ) expression
79
- 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 | string_with_interpolation
81
+ object_elem_key_expression : LPAR expression RPAR
80
82
  object_elem_key_dot_accessor : identifier (DOT identifier)+
81
83
 
82
84
  heredoc_template : /<<(?P<heredoc>[a-zA-Z][a-zA-Z0-9._-]+)\n?(?:.|\n)*?\n\s*(?P=heredoc)\n/
@@ -98,7 +100,7 @@ full_splat : "[*]" (get_attr | index)*
98
100
 
99
101
  FOR_OBJECT_ARROW : "=>"
100
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? "]"
101
- !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? "}"
102
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?
103
105
  !for_cond : "if" new_line_or_comment? expression
104
106
 
@@ -260,6 +260,7 @@ class HCLReconstructor(Reconstructor):
260
260
  Terminal("STRING_LIT"),
261
261
  Terminal("DECIMAL"),
262
262
  Terminal("NAME"),
263
+ Terminal("NEGATIVE_DECIMAL"),
263
264
  ]:
264
265
  return True
265
266
 
@@ -352,11 +353,21 @@ class HCLReverseTransformer:
352
353
 
353
354
  @staticmethod
354
355
  def _escape_interpolated_str(interp_s: str) -> str:
355
- # begin by doing basic JSON string escaping, to add backslashes
356
- interp_s = json.dumps(interp_s)
357
-
356
+ if interp_s.strip().startswith('<<-') or interp_s.strip().startswith('<<'):
357
+ # For heredoc strings, preserve their format exactly
358
+ return reverse_quotes_within_interpolation(interp_s)
359
+ # Escape backslashes first (very important to do this first)
360
+ escaped = interp_s.replace('\\', '\\\\')
361
+ # Escape quotes
362
+ escaped = escaped.replace('"', '\\"')
363
+ # 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')
358
369
  # find each interpolation within the string and remove the backslashes
359
- interp_s = reverse_quotes_within_interpolation(interp_s)
370
+ interp_s = reverse_quotes_within_interpolation(f'"{escaped}"')
360
371
  return interp_s
361
372
 
362
373
  @staticmethod
@@ -412,10 +423,7 @@ class HCLReverseTransformer:
412
423
  def _is_block(self, value: Any) -> bool:
413
424
  if isinstance(value, dict):
414
425
  block_body = value
415
- if (
416
- START_LINE_KEY in block_body.keys()
417
- or END_LINE_KEY in block_body.keys()
418
- ):
426
+ if START_LINE_KEY in block_body.keys() or END_LINE_KEY in block_body.keys():
419
427
  return True
420
428
 
421
429
  try:
@@ -520,7 +528,7 @@ class HCLReverseTransformer:
520
528
 
521
529
  return Tree(Token("RULE", "body"), children)
522
530
 
523
- # pylint: disable=too-many-branches, too-many-return-statements
531
+ # pylint: disable=too-many-branches, too-many-return-statements too-many-statements
524
532
  def _transform_value_to_expr_term(self, value, level) -> Union[Token, Tree]:
525
533
  """Transforms a value from a dictionary into an "expr_term" (a value in HCL2)
526
534
 
@@ -611,6 +619,37 @@ class HCLReverseTransformer:
611
619
  ],
612
620
  )
613
621
 
622
+ if isinstance(value, float):
623
+ value = str(value)
624
+ literal = []
625
+
626
+ if value[0] == "-":
627
+ # pop two first chars - minus and a digit
628
+ literal.append(Token("NEGATIVE_DECIMAL", value[:2]))
629
+ value = value[2:]
630
+
631
+ while value != "":
632
+ char = value[0]
633
+
634
+ if char == ".":
635
+ # current char marks beginning of decimal part: pop all remaining chars and end the loop
636
+ literal.append(Token("DOT", char))
637
+ literal.extend(Token("DECIMAL", char) for char in value[1:])
638
+ break
639
+
640
+ if char == "e":
641
+ # current char marks beginning of e-notation: pop all remaining chars and end the loop
642
+ literal.append(Token("EXP_MARK", value))
643
+ break
644
+
645
+ literal.append(Token("DECIMAL", char))
646
+ value = value[1:]
647
+
648
+ return Tree(
649
+ Token("RULE", "expr_term"),
650
+ [Tree(Token("RULE", "float_lit"), literal)],
651
+ )
652
+
614
653
  # store strings as single literals
615
654
  if isinstance(value, str):
616
655
  # 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
- return float("".join([self.to_tf_inline(arg) for arg in args]))
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,21 +101,21 @@ 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
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]
106
- else:
107
- key = self.strip_quotes(str(args[0].children[0]))
108
- value = args[2]
109
-
110
- value = self.to_string_dollar(value)
104
+
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)
109
+
110
+ value = self.to_string_dollar(args[2])
111
111
  return {key: value}
112
112
 
113
113
  def object_elem_key_dot_accessor(self, args: List) -> str:
114
114
  return "".join(args)
115
115
 
116
+ def object_elem_key_expression(self, args: List) -> str:
117
+ return self.to_string_dollar("".join(args))
118
+
116
119
  def object(self, args: List) -> Dict:
117
120
  args = self.strip_new_line_tokens(args)
118
121
  result: Dict[str, Any] = {}
@@ -179,7 +182,9 @@ class DictTransformer(Transformer):
179
182
  return f"{args[0]} ? {args[1]} : {args[2]}"
180
183
 
181
184
  def binary_op(self, args: List) -> str:
182
- return " ".join([self.to_tf_inline(arg) for arg in args])
185
+ return " ".join(
186
+ [self.unwrap_string_dollar(self.to_tf_inline(arg)) for arg in args]
187
+ )
183
188
 
184
189
  def unary_op(self, args: List) -> str:
185
190
  args = self.process_nulls(args)
@@ -306,21 +311,31 @@ class DictTransformer(Transformer):
306
311
  """
307
312
  return [arg for arg in args if arg != "\n" and arg is not Discard]
308
313
 
314
+ def is_string_dollar(self, value: str) -> bool:
315
+ if not isinstance(value, str):
316
+ return False
317
+ return value.startswith("${") and value.endswith("}")
318
+
309
319
  def to_string_dollar(self, value: Any) -> Any:
310
320
  """Wrap a string in ${ and }"""
311
- if isinstance(value, str):
321
+ if not isinstance(value, str):
322
+ return value
312
323
  # if it's already wrapped, pass it unmodified
313
- if value.startswith("${") and value.endswith("}"):
314
- return value
324
+ if self.is_string_dollar(value):
325
+ return value
315
326
 
316
- if value.startswith('"') and value.endswith('"'):
317
- value = str(value)[1:-1]
318
- return self.process_escape_sequences(value)
327
+ if value.startswith('"') and value.endswith('"'):
328
+ value = str(value)[1:-1]
329
+ return self.process_escape_sequences(value)
330
+
331
+ if self.is_type_keyword(value):
332
+ return value
319
333
 
320
- if self.is_type_keyword(value):
321
- return value
334
+ return f"${{{value}}}"
322
335
 
323
- return f"${{{value}}}"
336
+ def unwrap_string_dollar(self, value: str):
337
+ if self.is_string_dollar(value):
338
+ return value[2:-1]
324
339
  return value
325
340
 
326
341
  def strip_quotes(self, value: Any) -> Any:
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '7.1.0'
21
- __version_tuple__ = version_tuple = (7, 1, 0)
20
+ __version__ = version = '7.2.1'
21
+ __version_tuple__ = version_tuple = (7, 2, 1)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-hcl2
3
- Version: 7.1.0
3
+ Version: 7.2.1
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
- ### Error parsing string interpolations nested more than 2 times
117
+ ### Using inline expression as an object key
118
118
 
119
- - Parsing following example is expected to throw out an exception and fail:
119
+ - Object key can be an expression as long as it is wrapped in parentheses:
120
120
  ```terraform
121
121
  locals {
122
- foo = "foo"
123
- name = "prefix1-${"prefix2-${"${local.foo}-bar"}"}" //should interpolate into "prefix1-prefix2-foo-bar" but fails
124
- }
125
- ```
126
- We recommend working around this by modifying the configuration in the following manner:
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
  ```
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