python-hcl2 6.1.1__tar.gz → 7.0.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.0.1/.github/ISSUE_TEMPLATE/hcl2-parsing-error.md +32 -0
  2. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/.github/workflows/publish.yml +3 -3
  3. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/CHANGELOG.md +26 -0
  4. {python-hcl2-6.1.1/python_hcl2.egg-info → python_hcl2-7.0.1}/PKG-INFO +25 -3
  5. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/README.md +21 -1
  6. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/hcl2/__init__.py +0 -1
  7. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/hcl2/api.py +14 -29
  8. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/hcl2/hcl2.lark +17 -13
  9. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/hcl2/reconstructor.py +82 -87
  10. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/hcl2/transformer.py +23 -12
  11. python_hcl2-7.0.1/hcl2/version.py +21 -0
  12. {python-hcl2-6.1.1 → python_hcl2-7.0.1/python_hcl2.egg-info}/PKG-INFO +25 -3
  13. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/python_hcl2.egg-info/SOURCES.txt +1 -0
  14. python-hcl2-6.1.1/hcl2/version.py +0 -4
  15. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/.codacy.yml +0 -0
  16. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/.coveragerc +0 -0
  17. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/.github/CODEOWNERS +0 -0
  18. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/.github/workflows/codeql-analysis.yml +0 -0
  19. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/.github/workflows/pr_check.yml +0 -0
  20. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/.gitignore +0 -0
  21. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/.pre-commit-config.yaml +0 -0
  22. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/.yamllint.yml +0 -0
  23. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/LICENSE +0 -0
  24. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/MANIFEST.in +0 -0
  25. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/bin/terraform_test +0 -0
  26. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/hcl2/__main__.py +0 -0
  27. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/hcl2/builder.py +0 -0
  28. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/hcl2/parser.py +0 -0
  29. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/hcl2/py.typed +0 -0
  30. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/mypy.ini +0 -0
  31. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/pylintrc +0 -0
  32. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/pyproject.toml +0 -0
  33. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/python_hcl2.egg-info/dependency_links.txt +0 -0
  34. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/python_hcl2.egg-info/entry_points.txt +0 -0
  35. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/python_hcl2.egg-info/not-zip-safe +0 -0
  36. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/python_hcl2.egg-info/requires.txt +0 -0
  37. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/python_hcl2.egg-info/top_level.txt +0 -0
  38. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/reports/.gitignore +0 -0
  39. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/requirements.txt +0 -0
  40. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/setup.cfg +0 -0
  41. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/test-requirements.txt +0 -0
  42. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/tox.ini +0 -0
  43. {python-hcl2-6.1.1 → python_hcl2-7.0.1}/tree-to-hcl2-reconstruction.md +0 -0
@@ -0,0 +1,32 @@
1
+ ---
2
+ name: HCL2 parsing error
3
+ about: Template for reporting a bug related to parsing HCL2 code
4
+ title: ''
5
+ labels: bug
6
+ assignees: kkozik-amplify
7
+
8
+ ---
9
+
10
+ **Describe the bug**
11
+
12
+ A clear and concise description of what the bug is.
13
+
14
+ **Software:**
15
+ - OS: [macOS / Windows / Linux]
16
+ - Python version (e.g. 3.9.21)
17
+ - python-hcl2 version (e.g. 7.0.0)
18
+
19
+ **Snippet of HCL2 code causing the unexpected behaviour:**
20
+ ```terraform
21
+ locals {
22
+ foo = "bar"
23
+ }
24
+ ```
25
+ **Expected behavior**
26
+
27
+ A clear and concise description of what you expected to happen, e.g. python dictionary or JSON you expected to receive as a result of parsing.
28
+
29
+ **Exception traceback (if applicable)**:
30
+
31
+ ```
32
+ ```
@@ -13,7 +13,7 @@ jobs:
13
13
  - name: Set up Python
14
14
  uses: actions/setup-python@v2
15
15
  with:
16
- python-version: 3.7
16
+ python-version: 3.13
17
17
  - name: Install dependencies
18
18
  run: python -m pip install --upgrade pip build
19
19
  - name: Generate Lark Parser
@@ -23,12 +23,12 @@ jobs:
23
23
  - name: Build tarball
24
24
  run: python3 -m build
25
25
  - name: Publish to Test PyPI
26
- uses: pypa/gh-action-pypi-publish@master
26
+ uses: pypa/gh-action-pypi-publish@release/v1
27
27
  with:
28
28
  password: ${{ secrets.TEST_PYPI_API_TOKEN }}
29
29
  repository_url: https://test.pypi.org/legacy/
30
30
  skip_existing: true
31
31
  - name: Publish to PyPI
32
- uses: pypa/gh-action-pypi-publish@master
32
+ uses: pypa/gh-action-pypi-publish@release/v1
33
33
  with:
34
34
  password: ${{ secrets.PYPI_API_TOKEN }}
@@ -5,6 +5,32 @@ 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
+ ## \[7.0.1\] - 2025-03-31
9
+
10
+ ### Fixed
11
+
12
+ - Issue parsing dot-accessed attribute as an object key ([#209](https://github.com/amplify-education/python-hcl2/pull/209))
13
+
14
+ ## \[7.0.0\] - 2025-03-27
15
+
16
+ ### Added
17
+
18
+ - `Limitations` section to README.md ([#200](https://github.com/amplify-education/python-hcl2/pull/200))
19
+
20
+ ### Fixed
21
+
22
+ - Issue handling heredoc with delimiter within text itself ([#194](https://github.com/amplify-education/python-hcl2/pull/194))
23
+ - Various issues with parsing object elements ([#197](https://github.com/amplify-education/python-hcl2/pull/197))
24
+ - Dictionary -> hcl2 reconstruction of `null` values ([#198](https://github.com/amplify-education/python-hcl2/pull/198))
25
+ - Inaccurate parsing of `null` values in some cases ([#206](https://github.com/amplify-education/python-hcl2/pull/206))
26
+ - Missing parenthesis in arithemetic expressions ([#194](https://github.com/amplify-education/python-hcl2/pull/199))
27
+ - Noticeable overhead when loading hcl2.reconstructor module ([#202](https://github.com/amplify-education/python-hcl2/pull/202))
28
+ - Escaped string interpolation (e.g. `"$${aws:username}"`) parsing ([#200](https://github.com/amplify-education/python-hcl2/pull/200))
29
+
30
+ ### Removed
31
+
32
+ - Support for parsing interpolations nested more than 2 times (known-issue) ([#200](https://github.com/amplify-education/python-hcl2/pull/200))
33
+
8
34
  ## \[6.1.1\] - 2025-02-13
9
35
 
10
36
  ### Fixed
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: python-hcl2
3
- Version: 6.1.1
3
+ Version: 7.0.1
4
4
  Summary: A parser for HCL2
5
5
  Author-email: Amplify Education <github@amplify.com>
6
6
  License: MIT
@@ -22,6 +22,8 @@ Classifier: Programming Language :: Python :: 3.13
22
22
  Requires-Python: >=3.7.0
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
+ Requires-Dist: lark<2,>=1
26
+ Dynamic: license-file
25
27
 
26
28
  [![Codacy Badge](https://app.codacy.com/project/badge/Grade/2e2015f9297346cbaa788c46ab957827)](https://app.codacy.com/gh/amplify-education/python-hcl2/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
27
29
  [![Build Status](https://travis-ci.org/amplify-education/python-hcl2.svg?branch=master)](https://travis-ci.org/amplify-education/python-hcl2)
@@ -92,7 +94,7 @@ To see all the available options, run `tox -l`.
92
94
 
93
95
  ## Releasing
94
96
 
95
- To create a new releaes go to Releases page, press 'Draft a new release', create a tag
97
+ To create a new release go to Releases page, press 'Draft a new release', create a tag
96
98
  with a version you want to be released, fill the release notes and press 'Publish release'.
97
99
  Github actions will take care of publishing it to PyPi.
98
100
 
@@ -109,3 +111,23 @@ We welcome pull requests! For your pull request to be accepted smoothly, we sugg
109
111
  - Create a pull request. Explain why you want to make the change and what it’s for.
110
112
 
111
113
  We’ll try to answer any PR’s promptly.
114
+
115
+ ## Limitations
116
+
117
+ ### Error parsing string interpolations nested more than 2 times
118
+
119
+ - Parsing following example is expected to throw out an exception and fail:
120
+ ```terraform
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"
132
+ }
133
+ ```
@@ -67,7 +67,7 @@ To see all the available options, run `tox -l`.
67
67
 
68
68
  ## Releasing
69
69
 
70
- To create a new releaes go to Releases page, press 'Draft a new release', create a tag
70
+ To create a new release go to Releases page, press 'Draft a new release', create a tag
71
71
  with a version you want to be released, fill the release notes and press 'Publish release'.
72
72
  Github actions will take care of publishing it to PyPi.
73
73
 
@@ -84,3 +84,23 @@ We welcome pull requests! For your pull request to be accepted smoothly, we sugg
84
84
  - Create a pull request. Explain why you want to make the change and what it’s for.
85
85
 
86
86
  We’ll try to answer any PR’s promptly.
87
+
88
+ ## Limitations
89
+
90
+ ### Error parsing string interpolations nested more than 2 times
91
+
92
+ - Parsing following example is expected to throw out an exception and fail:
93
+ ```terraform
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"
105
+ }
106
+ ```
@@ -13,7 +13,6 @@ from .api import (
13
13
  transform,
14
14
  reverse_transform,
15
15
  writes,
16
- AST,
17
16
  )
18
17
 
19
18
  from .builder import Builder
@@ -1,9 +1,10 @@
1
1
  """The API that will be exposed to users of this package"""
2
2
  from typing import TextIO
3
3
 
4
- from lark.tree import Tree as AST
5
- from hcl2.parser import parser
4
+ from lark.tree import Tree
5
+ from hcl2.parser import parser, reconstruction_parser
6
6
  from hcl2.transformer import DictTransformer
7
+ from hcl2.reconstructor import HCLReconstructor, HCLReverseTransformer
7
8
 
8
9
 
9
10
  def load(file: TextIO, with_meta=False) -> dict:
@@ -22,61 +23,45 @@ def loads(text: str, with_meta=False) -> dict:
22
23
  parameters to the output dict. Default to false.
23
24
  """
24
25
  # append new line as a workaround for https://github.com/lark-parser/lark/issues/237
25
- # Lark doesn't support a EOF token so our grammar can't look for "new line or end of file"
26
+ # Lark doesn't support EOF token so our grammar can't look for "new line or end of file"
26
27
  # This means that all blocks must end in a new line even if the file ends
27
28
  # Append a new line as a temporary fix
28
29
  tree = parser().parse(text + "\n")
29
30
  return DictTransformer(with_meta=with_meta).transform(tree)
30
31
 
31
32
 
32
- def parse(file: TextIO) -> AST:
33
+ def parse(file: TextIO) -> Tree:
33
34
  """Load HCL2 syntax tree from a file.
34
35
  :param file: File with hcl2 to be loaded as a dict.
35
36
  """
36
37
  return parses(file.read())
37
38
 
38
39
 
39
- def parses(text: str) -> AST:
40
+ def parses(text: str) -> Tree:
40
41
  """Load HCL2 syntax tree from a string.
41
42
  :param text: Text with hcl2 to be loaded as a dict.
42
43
  """
43
- # defer this import until this method is called, due to the performance hit
44
- # of rebuilding the grammar without cache
45
- from hcl2.parser import ( # pylint: disable=import-outside-toplevel
46
- reconstruction_parser,
47
- )
48
-
49
44
  return reconstruction_parser().parse(text)
50
45
 
51
46
 
52
- def transform(ast: AST, with_meta=False) -> dict:
47
+ def transform(ast: Tree, with_meta=False) -> dict:
53
48
  """Convert an HCL2 AST to a dictionary.
54
49
  :param ast: HCL2 syntax tree, output from `parse` or `parses`
50
+ :param with_meta: If set to true then adds `__start_line__` and `__end_line__`
51
+ parameters to the output dict. Default to false.
55
52
  """
56
53
  return DictTransformer(with_meta=with_meta).transform(ast)
57
54
 
58
55
 
59
- def reverse_transform(hcl2_dict: dict) -> AST:
56
+ def reverse_transform(hcl2_dict: dict) -> Tree:
60
57
  """Convert a dictionary to an HCL2 AST.
61
- :param dict: a dictionary produced by `load` or `transform`
58
+ :param hcl2_dict: a dictionary produced by `load` or `transform`
62
59
  """
63
- # defer this import until this method is called, due to the performance hit
64
- # of rebuilding the grammar without cache
65
- from hcl2.reconstructor import ( # pylint: disable=import-outside-toplevel
66
- hcl2_reverse_transformer,
67
- )
60
+ return HCLReverseTransformer().transform(hcl2_dict)
68
61
 
69
- return hcl2_reverse_transformer.transform(hcl2_dict)
70
62
 
71
-
72
- def writes(ast: AST) -> str:
63
+ def writes(ast: Tree) -> str:
73
64
  """Convert an HCL2 syntax tree to a string.
74
65
  :param ast: HCL2 syntax tree, output from `parse` or `parses`
75
66
  """
76
- # defer this import until this method is called, due to the performance hit
77
- # of rebuilding the grammar without cache
78
- from hcl2.reconstructor import ( # pylint: disable=import-outside-toplevel
79
- hcl2_reconstructor,
80
- )
81
-
82
- return hcl2_reconstructor.reconstruct(ast)
67
+ return HCLReconstructor(reconstruction_parser()).reconstruct(ast)
@@ -1,8 +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 "}"
5
- new_line_and_or_comma: new_line_or_comment | "," | "," new_line_or_comment
4
+ block : identifier (identifier | STRING_LIT)* new_line_or_comment? "{" body "}"
6
5
  new_line_or_comment: ( NL_OR_COMMENT )+
7
6
  NL_OR_COMMENT: /\n[ \t]*/ | /#.*\n/ | /\/\/.*\n/ | /\/\*(.|\n)*?(\*\/)/
8
7
 
@@ -36,12 +35,15 @@ PERCENT : "%"
36
35
  DOUBLE_AMP : "&&"
37
36
  DOUBLE_PIPE : "||"
38
37
  PLUS : "+"
38
+ LPAR : "("
39
+ RPAR : ")"
40
+ COMMA : ","
41
+ DOT : "."
39
42
 
40
- expr_term : "(" new_line_or_comment? expression new_line_or_comment? ")"
43
+ expr_term : LPAR new_line_or_comment? expression new_line_or_comment? RPAR
41
44
  | float_lit
42
45
  | int_lit
43
46
  | STRING_LIT
44
- | string_with_interpolation
45
47
  | tuple
46
48
  | object
47
49
  | function_call
@@ -56,10 +58,11 @@ expr_term : "(" new_line_or_comment? expression new_line_or_comment? ")"
56
58
  | for_tuple_expr
57
59
  | for_object_expr
58
60
 
59
- STRING_LIT : "\"" STRING_CHARS? "\""
60
- STRING_CHARS : /(?:(?!\${)([^"\\]|\\.))+/ // any character except '"'
61
- string_with_interpolation: "\"" (STRING_CHARS)* interpolation_maybe_nested (STRING_CHARS | interpolation_maybe_nested)* "\""
62
- interpolation_maybe_nested: "${" expression "}"
61
+ STRING_LIT : "\"" (STRING_CHARS | INTERPOLATION)* "\""
62
+ STRING_CHARS : /(?:(?!\${)([^"\\]|\\.))+/+ // any character except '"" unless inside a interpolation string
63
+ NESTED_INTERPOLATION : "${" /[^}]+/ "}"
64
+ INTERPOLATION : "${" (/(?:(?!\${)([^}]))+/ | NESTED_INTERPOLATION)+ "}"
65
+
63
66
 
64
67
  int_lit : NEGATIVE_DECIMAL? DECIMAL+ | NEGATIVE_DECIMAL+
65
68
  !float_lit: (NEGATIVE_DECIMAL? DECIMAL+ | NEGATIVE_DECIMAL+) "." DECIMAL+ (EXP_MARK)?
@@ -70,12 +73,13 @@ EXP_MARK : ("e" | "E") ("+" | "-")? DECIMAL+
70
73
  EQ : /[ \t]*=(?!=|>)/
71
74
 
72
75
  tuple : "[" (new_line_or_comment* expression new_line_or_comment* ",")* (new_line_or_comment* expression)? new_line_or_comment* "]"
73
- object : "{" new_line_or_comment? (object_elem (new_line_and_or_comma object_elem )* new_line_and_or_comma?)? "}"
74
- object_elem : (identifier | expression) ( EQ | ":") expression
75
-
76
+ 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 | ":") expression
78
+ object_elem_key : float_lit | int_lit | identifier | STRING_LIT | object_elem_key_dot_accessor
79
+ object_elem_key_dot_accessor : identifier (DOT identifier)+
76
80
 
77
- heredoc_template : /<<(?P<heredoc>[a-zA-Z][a-zA-Z0-9._-]+)\n(?:.|\n)*?(?P=heredoc)/
78
- heredoc_template_trim : /<<-(?P<heredoc_trim>[a-zA-Z][a-zA-Z0-9._-]+)\n(?:.|\n)*?(?P=heredoc_trim)/
81
+ heredoc_template : /<<(?P<heredoc>[a-zA-Z][a-zA-Z0-9._-]+)\n?(?:.|\n)*?\n\s*(?P=heredoc)\n/
82
+ heredoc_template_trim : /<<-(?P<heredoc_trim>[a-zA-Z][a-zA-Z0-9._-]+)\n?(?:.|\n)*?\n\s*(?P=heredoc_trim)\n/
79
83
 
80
84
  function_call : identifier "(" new_line_or_comment? arguments? new_line_or_comment? ")"
81
85
  arguments : (expression (new_line_or_comment* "," new_line_or_comment* expression)* ("," | "...")? new_line_or_comment*)
@@ -22,7 +22,7 @@ def reverse_quotes_within_interpolation(interp_s: str) -> str:
22
22
  method removes any erroneous escapes within interpolated segments of a
23
23
  string.
24
24
  """
25
- return re.sub(r"\$\{(.*)\}", lambda m: m.group(0).replace('\\"', '"'), interp_s)
25
+ return re.sub(r"\$\{(.*)}", lambda m: m.group(0).replace('\\"', '"'), interp_s)
26
26
 
27
27
 
28
28
  class WriteTokensAndMetaTransformer(Transformer_InPlace):
@@ -43,6 +43,7 @@ class WriteTokensAndMetaTransformer(Transformer_InPlace):
43
43
  tokens: Dict[str, TerminalDef],
44
44
  term_subs: Dict[str, Callable[[Symbol], str]],
45
45
  ) -> None:
46
+ super().__init__()
46
47
  self.tokens = tokens
47
48
  self.term_subs = term_subs
48
49
 
@@ -91,14 +92,6 @@ class WriteTokensAndMetaTransformer(Transformer_InPlace):
91
92
  class HCLReconstructor(Reconstructor):
92
93
  """This class converts a Lark.Tree AST back into a string representing the underlying HCL code."""
93
94
 
94
- # these variables track state during reconstruction to enable us to make
95
- # informed decisions about formatting output. They are primarily used
96
- # by the _should_add_space(...) method.
97
- last_char_space = True
98
- last_terminal = None
99
- last_rule = None
100
- deferred_item = None
101
-
102
95
  def __init__(
103
96
  self,
104
97
  parser: Lark,
@@ -106,32 +99,38 @@ class HCLReconstructor(Reconstructor):
106
99
  ):
107
100
  Reconstructor.__init__(self, parser, term_subs)
108
101
 
109
- self.write_tokens = WriteTokensAndMetaTransformer(
110
- {token.name: token for token in self.tokens}, term_subs or {}
102
+ self.write_tokens: WriteTokensAndMetaTransformer = (
103
+ WriteTokensAndMetaTransformer(
104
+ {token.name: token for token in self.tokens}, term_subs or {}
105
+ )
111
106
  )
112
107
 
113
- # space around these terminals if they're within for or if statements
114
- FOR_IF_KEYWORDS = [
115
- Terminal("IF"),
116
- Terminal("IN"),
117
- Terminal("FOR"),
118
- Terminal("FOR_EACH"),
119
- Terminal("FOR_OBJECT_ARROW"),
120
- Terminal("COLON"),
121
- ]
122
-
123
- # space on both sides, in ternaries and binary operators
124
- BINARY_OPS = [
125
- Terminal("QMARK"),
126
- Terminal("COLON"),
127
- Terminal("BINARY_OP"),
128
- ]
108
+ # these variables track state during reconstruction to enable us to make
109
+ # informed decisions about formatting output. They are primarily used
110
+ # by the _should_add_space(...) method.
111
+ self._last_char_space = True
112
+ self._last_terminal: Union[Terminal, None] = None
113
+ self._last_rule: Union[Tree, Token, None] = None
114
+ self._deferred_item = None
115
+
116
+ def should_be_wrapped_in_spaces(self, terminal: Terminal) -> bool:
117
+ """Whether given terminal should be wrapped in spaces"""
118
+ return terminal.name in {
119
+ "IF",
120
+ "IN",
121
+ "FOR",
122
+ "FOR_EACH",
123
+ "FOR_OBJECT_ARROW",
124
+ "COLON",
125
+ "QMARK",
126
+ "BINARY_OP",
127
+ }
129
128
 
130
129
  def _is_equals_sign(self, terminal) -> bool:
131
130
  return (
132
- isinstance(self.last_rule, Token)
133
- and self.last_rule.value in ("attribute", "object_elem")
134
- and self.last_terminal == Terminal("EQ")
131
+ isinstance(self._last_rule, Token)
132
+ and self._last_rule.value in ("attribute", "object_elem")
133
+ and self._last_terminal == Terminal("EQ")
135
134
  and terminal != Terminal("NL_OR_COMMENT")
136
135
  )
137
136
 
@@ -155,11 +154,11 @@ class HCLReconstructor(Reconstructor):
155
154
  This should be sufficient to make a spacing decision.
156
155
  """
157
156
  # we don't need to add multiple spaces
158
- if self.last_char_space:
157
+ if self._last_char_space:
159
158
  return False
160
159
 
161
160
  # we don't add a space at the start of the file
162
- if not self.last_terminal or not self.last_rule:
161
+ if not self._last_terminal or not self._last_rule:
163
162
  return False
164
163
 
165
164
  if self._is_equals_sign(current_terminal):
@@ -173,20 +172,20 @@ class HCLReconstructor(Reconstructor):
173
172
  "conditional",
174
173
  "binary_operator",
175
174
  ]
176
- and current_terminal in self.BINARY_OPS
175
+ and self.should_be_wrapped_in_spaces(current_terminal)
177
176
  ):
178
177
  return True
179
178
 
180
179
  # if we just left a ternary or binary operator, add space around the
181
180
  # operator unless there's a newline already
182
181
  if (
183
- isinstance(self.last_rule, Token)
184
- and self.last_rule.value
182
+ isinstance(self._last_rule, Token)
183
+ and self._last_rule.value
185
184
  in [
186
185
  "conditional",
187
186
  "binary_operator",
188
187
  ]
189
- and self.last_terminal in self.BINARY_OPS
188
+ and self.should_be_wrapped_in_spaces(self._last_terminal)
190
189
  and current_terminal != Terminal("NL_OR_COMMENT")
191
190
  ):
192
191
  return True
@@ -200,21 +199,21 @@ class HCLReconstructor(Reconstructor):
200
199
  "for_cond",
201
200
  "for_intro",
202
201
  ]
203
- and current_terminal in self.FOR_IF_KEYWORDS
202
+ and self.should_be_wrapped_in_spaces(current_terminal)
204
203
  ):
205
204
  return True
206
205
 
207
206
  # if we've just left a for or if statement and find a keyword, add a
208
207
  # space, unless we have a newline
209
208
  if (
210
- isinstance(self.last_rule, Token)
211
- and self.last_rule.value
209
+ isinstance(self._last_rule, Token)
210
+ and self._last_rule.value
212
211
  in [
213
212
  "for_object_expr",
214
213
  "for_cond",
215
214
  "for_intro",
216
215
  ]
217
- and self.last_terminal in self.FOR_IF_KEYWORDS
216
+ and self.should_be_wrapped_in_spaces(self._last_terminal)
218
217
  and current_terminal != Terminal("NL_OR_COMMENT")
219
218
  ):
220
219
  return True
@@ -230,7 +229,7 @@ class HCLReconstructor(Reconstructor):
230
229
  # always add space before the closing brace
231
230
  if current_terminal == Terminal(
232
231
  "RBRACE"
233
- ) and self.last_terminal != Terminal("LBRACE"):
232
+ ) and self._last_terminal != Terminal("LBRACE"):
234
233
  return True
235
234
 
236
235
  # always add space between string literals
@@ -240,20 +239,20 @@ class HCLReconstructor(Reconstructor):
240
239
  # if we just opened a block, add a space, unless the block is empty
241
240
  # or has a newline
242
241
  if (
243
- isinstance(self.last_rule, Token)
244
- and self.last_rule.value == "block"
245
- and self.last_terminal == Terminal("LBRACE")
242
+ isinstance(self._last_rule, Token)
243
+ and self._last_rule.value == "block"
244
+ and self._last_terminal == Terminal("LBRACE")
246
245
  and current_terminal not in [Terminal("RBRACE"), Terminal("NL_OR_COMMENT")]
247
246
  ):
248
247
  return True
249
248
 
250
249
  # if we're in a tuple or function arguments (this rule matches commas between items)
251
- if isinstance(self.last_rule, str) and re.match(
252
- r"^__(tuple|arguments)_(star|plus)_.*", self.last_rule
250
+ if isinstance(self._last_rule, str) and re.match(
251
+ r"^__(tuple|arguments)_(star|plus)_.*", self._last_rule
253
252
  ):
254
253
 
255
254
  # string literals, decimals, and identifiers should always be
256
- # preceeded by a space if they're following a comma in a tuple or
255
+ # preceded by a space if they're following a comma in a tuple or
257
256
  # function arg
258
257
  if current_terminal in [
259
258
  Terminal("STRING_LIT"),
@@ -279,12 +278,12 @@ class HCLReconstructor(Reconstructor):
279
278
  rule, terminal, value = item
280
279
 
281
280
  # first, handle any deferred items
282
- if self.deferred_item is not None:
281
+ if self._deferred_item is not None:
283
282
  (
284
283
  deferred_rule,
285
284
  deferred_terminal,
286
285
  deferred_value,
287
- ) = self.deferred_item
286
+ ) = self._deferred_item
288
287
 
289
288
  # if we deferred a comma and the next character ends a
290
289
  # parenthesis or block, we can throw it out
@@ -298,32 +297,32 @@ class HCLReconstructor(Reconstructor):
298
297
  yield deferred_value
299
298
 
300
299
  # and do our bookkeeping
301
- self.last_terminal = deferred_terminal
302
- self.last_rule = deferred_rule
300
+ self._last_terminal = deferred_terminal
301
+ self._last_rule = deferred_rule
303
302
  if deferred_value and not deferred_value[-1].isspace():
304
- self.last_char_space = False
303
+ self._last_char_space = False
305
304
 
306
305
  # clear the deferred item
307
- self.deferred_item = None
306
+ self._deferred_item = None
308
307
 
309
308
  # potentially add a space before the next token
310
309
  if self._should_add_space(rule, terminal):
311
310
  yield " "
312
- self.last_char_space = True
311
+ self._last_char_space = True
313
312
 
314
- # potentially defer the item if needs to be
313
+ # potentially defer the item if needed
315
314
  if terminal in [Terminal("COMMA")]:
316
- self.deferred_item = item
315
+ self._deferred_item = item
317
316
  else:
318
317
  # otherwise print the next token
319
318
  yield value
320
319
 
321
320
  # and do our bookkeeping so we can make an informed
322
321
  # decision about formatting next time
323
- self.last_terminal = terminal
324
- self.last_rule = rule
322
+ self._last_terminal = terminal
323
+ self._last_rule = rule
325
324
  if value:
326
- self.last_char_space = value[-1].isspace()
325
+ self._last_char_space = value[-1].isspace()
327
326
 
328
327
  else:
329
328
  raise RuntimeError(f"Unknown bare token type: {item}")
@@ -375,7 +374,7 @@ class HCLReverseTransformer:
375
374
  @staticmethod
376
375
  def _is_string_wrapped_tf(interp_s: str) -> bool:
377
376
  """
378
- Determines whether a string is a complex HCL datastructure
377
+ Determines whether a string is a complex HCL data structure
379
378
  wrapped in ${ interpolation } characters.
380
379
  """
381
380
  if not interp_s.startswith("${") or not interp_s.endswith("}"):
@@ -396,15 +395,7 @@ class HCLReverseTransformer:
396
395
 
397
396
  return True
398
397
 
399
- def _newline(self, level: int, comma: bool = False, count: int = 1) -> Tree:
400
- # some rules expect the `new_line_and_or_comma` token
401
- if comma:
402
- return Tree(
403
- Token("RULE", "new_line_and_or_comma"),
404
- [self._newline(level=level, comma=False, count=count)],
405
- )
406
-
407
- # otherwise, return the `new_line_or_comment` token
398
+ def _newline(self, level: int, count: int = 1) -> Tree:
408
399
  return Tree(
409
400
  Token("RULE", "new_line_or_comment"),
410
401
  [Token("NL_OR_COMMENT", f"\n{' ' * level}") for _ in range(count)],
@@ -548,35 +539,48 @@ class HCLReverseTransformer:
548
539
  )
549
540
  return Tree(Token("RULE", "expr_term"), [tuple_tree])
550
541
 
542
+ if value is None:
543
+ return Tree(
544
+ Token("RULE", "expr_term"),
545
+ [Tree(Token("RULE", "identifier"), [Token("NAME", "null")])],
546
+ )
547
+
551
548
  # for dicts, recursively turn the child k/v pairs into object elements
552
549
  # and store within an object
553
550
  if isinstance(value, dict):
554
- elems = []
551
+ elements = []
555
552
 
556
553
  # if the object has elements, put it on a newline
557
554
  if len(value) > 0:
558
- elems.append(self._newline(level + 1))
555
+ elements.append(self._newline(level + 1))
559
556
 
560
557
  # iterate through the items and add them to the object
561
558
  for i, (k, dict_v) in enumerate(value.items()):
562
559
  if k in ["__start_line__", "__end_line__"]:
563
560
  continue
564
- identifier = self._name_to_identifier(k)
561
+
565
562
  value_expr_term = self._transform_value_to_expr_term(dict_v, level + 1)
566
- elems.append(
563
+ elements.append(
567
564
  Tree(
568
565
  Token("RULE", "object_elem"),
569
- [identifier, Token("EQ", " ="), value_expr_term],
566
+ [
567
+ Tree(
568
+ Token("RULE", "object_elem_key"),
569
+ [Tree(Token("RULE", "identifier"), [Token("NAME", k)])],
570
+ ),
571
+ Token("EQ", " ="),
572
+ value_expr_term,
573
+ ],
570
574
  )
571
575
  )
572
576
 
573
577
  # add indentation appropriately
574
578
  if i < len(value) - 1:
575
- elems.append(self._newline(level + 1, comma=True))
579
+ elements.append(self._newline(level + 1))
576
580
  else:
577
- elems.append(self._newline(level, comma=True))
581
+ elements.append(self._newline(level))
578
582
  return Tree(
579
- Token("RULE", "expr_term"), [Tree(Token("RULE", "object"), elems)]
583
+ Token("RULE", "expr_term"), [Tree(Token("RULE", "object"), elements)]
580
584
  )
581
585
 
582
586
  # treat booleans appropriately
@@ -626,12 +630,7 @@ class HCLReverseTransformer:
626
630
  raise RuntimeError("Token must be `EQ (=)` rule")
627
631
 
628
632
  parsed_value = attribute.children[2]
629
-
630
- if parsed_value.data == Token("RULE", "expr_term"):
631
- return parsed_value
632
-
633
- # wrap other types of syntax as an expression (in parenthesis)
634
- return Tree(Token("RULE", "expr_term"), [parsed_value])
633
+ return parsed_value
635
634
 
636
635
  # otherwise it's just a string.
637
636
  return Tree(
@@ -641,7 +640,3 @@ class HCLReverseTransformer:
641
640
 
642
641
  # otherwise, we don't know the type
643
642
  raise RuntimeError(f"Unknown type to transform {type(value)}")
644
-
645
-
646
- hcl2_reconstructor = HCLReconstructor(reconstruction_parser())
647
- hcl2_reverse_transformer = HCLReverseTransformer()
@@ -5,6 +5,7 @@ import sys
5
5
  from collections import namedtuple
6
6
  from typing import List, Dict, Any
7
7
 
8
+ from lark import Token
8
9
  from lark.tree import Meta
9
10
  from lark.visitors import Transformer, Discard, _DiscardType, v_args
10
11
 
@@ -51,7 +52,6 @@ class DictTransformer(Transformer):
51
52
  def expr_term(self, args: List) -> Any:
52
53
  args = self.strip_new_line_tokens(args)
53
54
 
54
- #
55
55
  if args[0] == "true":
56
56
  return True
57
57
  if args[0] == "false":
@@ -59,10 +59,9 @@ class DictTransformer(Transformer):
59
59
  if args[0] == "null":
60
60
  return None
61
61
 
62
- # if the expression starts with a paren then unwrap it
63
- if args[0] == "(":
64
- return args[1]
65
- # otherwise return the value itself
62
+ if args[0] == "(" and args[-1] == ")":
63
+ return "".join(str(arg) for arg in args)
64
+
66
65
  return args[0]
67
66
 
68
67
  def index_expr_term(self, args: List) -> str:
@@ -99,18 +98,27 @@ class DictTransformer(Transformer):
99
98
  def object_elem(self, args: List) -> Dict:
100
99
  # This returns a dict with a single key/value pair to make it easier to merge these
101
100
  # into a bigger dict that is returned by the "object" function
102
- key = self.strip_quotes(args[0])
101
+ key = self.strip_quotes(str(args[0].children[0]))
103
102
  if len(args) == 3:
104
- value = self.to_string_dollar(args[2])
103
+ value = args[2]
105
104
  else:
106
- value = self.to_string_dollar(args[1])
105
+ value = args[1]
107
106
 
107
+ value = self.to_string_dollar(value)
108
108
  return {key: value}
109
109
 
110
+ def object_elem_key_dot_accessor(self, args: List) -> str:
111
+ return "".join(args)
112
+
110
113
  def object(self, args: List) -> Dict:
111
114
  args = self.strip_new_line_tokens(args)
112
115
  result: Dict[str, Any] = {}
113
116
  for arg in args:
117
+ if (
118
+ isinstance(arg, Token) and arg.type == "COMMA"
119
+ ): # skip optional comma at the end of object element
120
+ continue
121
+
114
122
  result.update(arg)
115
123
  return result
116
124
 
@@ -134,10 +142,7 @@ class DictTransformer(Transformer):
134
142
  return f"{provider_func}({args_str})"
135
143
 
136
144
  def arguments(self, args: List) -> List:
137
- return args
138
-
139
- def new_line_and_or_comma(self, args: List) -> _DiscardType:
140
- return Discard
145
+ return self.process_nulls(args)
141
146
 
142
147
  @v_args(meta=True)
143
148
  def block(self, meta: Meta, args: List) -> Dict:
@@ -167,16 +172,19 @@ class DictTransformer(Transformer):
167
172
 
168
173
  def conditional(self, args: List) -> str:
169
174
  args = self.strip_new_line_tokens(args)
175
+ args = self.process_nulls(args)
170
176
  return f"{args[0]} ? {args[1]} : {args[2]}"
171
177
 
172
178
  def binary_op(self, args: List) -> str:
173
179
  return " ".join([self.to_tf_inline(arg) for arg in args])
174
180
 
175
181
  def unary_op(self, args: List) -> str:
182
+ args = self.process_nulls(args)
176
183
  return "".join([self.to_tf_inline(arg) for arg in args])
177
184
 
178
185
  def binary_term(self, args: List) -> str:
179
186
  args = self.strip_new_line_tokens(args)
187
+ args = self.process_nulls(args)
180
188
  return " ".join([self.to_tf_inline(arg) for arg in args])
181
189
 
182
190
  def body(self, args: List) -> Dict[str, List]:
@@ -334,6 +342,9 @@ class DictTransformer(Transformer):
334
342
  # for now, but this method can be extended in the future
335
343
  return value
336
344
 
345
+ def process_nulls(self, args: List) -> List:
346
+ return ["null" if arg is None else arg for arg in args]
347
+
337
348
  def to_tf_inline(self, value: Any) -> str:
338
349
  """
339
350
  Converts complex objects (e.g.) dicts to an "inline" HCL syntax
@@ -0,0 +1,21 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
6
+ TYPE_CHECKING = False
7
+ if TYPE_CHECKING:
8
+ from typing import Tuple
9
+ from typing import Union
10
+
11
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
12
+ else:
13
+ VERSION_TUPLE = object
14
+
15
+ version: str
16
+ __version__: str
17
+ __version_tuple__: VERSION_TUPLE
18
+ version_tuple: VERSION_TUPLE
19
+
20
+ __version__ = version = '7.0.1'
21
+ __version_tuple__ = version_tuple = (7, 0, 1)
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: python-hcl2
3
- Version: 6.1.1
3
+ Version: 7.0.1
4
4
  Summary: A parser for HCL2
5
5
  Author-email: Amplify Education <github@amplify.com>
6
6
  License: MIT
@@ -22,6 +22,8 @@ Classifier: Programming Language :: Python :: 3.13
22
22
  Requires-Python: >=3.7.0
23
23
  Description-Content-Type: text/markdown
24
24
  License-File: LICENSE
25
+ Requires-Dist: lark<2,>=1
26
+ Dynamic: license-file
25
27
 
26
28
  [![Codacy Badge](https://app.codacy.com/project/badge/Grade/2e2015f9297346cbaa788c46ab957827)](https://app.codacy.com/gh/amplify-education/python-hcl2/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
27
29
  [![Build Status](https://travis-ci.org/amplify-education/python-hcl2.svg?branch=master)](https://travis-ci.org/amplify-education/python-hcl2)
@@ -92,7 +94,7 @@ To see all the available options, run `tox -l`.
92
94
 
93
95
  ## Releasing
94
96
 
95
- To create a new releaes go to Releases page, press 'Draft a new release', create a tag
97
+ To create a new release go to Releases page, press 'Draft a new release', create a tag
96
98
  with a version you want to be released, fill the release notes and press 'Publish release'.
97
99
  Github actions will take care of publishing it to PyPi.
98
100
 
@@ -109,3 +111,23 @@ We welcome pull requests! For your pull request to be accepted smoothly, we sugg
109
111
  - Create a pull request. Explain why you want to make the change and what it’s for.
110
112
 
111
113
  We’ll try to answer any PR’s promptly.
114
+
115
+ ## Limitations
116
+
117
+ ### Error parsing string interpolations nested more than 2 times
118
+
119
+ - Parsing following example is expected to throw out an exception and fail:
120
+ ```terraform
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"
132
+ }
133
+ ```
@@ -15,6 +15,7 @@ test-requirements.txt
15
15
  tox.ini
16
16
  tree-to-hcl2-reconstruction.md
17
17
  .github/CODEOWNERS
18
+ .github/ISSUE_TEMPLATE/hcl2-parsing-error.md
18
19
  .github/workflows/codeql-analysis.yml
19
20
  .github/workflows/pr_check.yml
20
21
  .github/workflows/publish.yml
@@ -1,4 +0,0 @@
1
- # file generated by setuptools_scm
2
- # don't change, don't track in version control
3
- __version__ = version = '6.1.1'
4
- __version_tuple__ = version_tuple = (6, 1, 1)
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