python-hcl2 7.0.0__tar.gz → 7.1.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 (44) hide show
  1. python_hcl2-7.1.0/.github/ISSUE_TEMPLATE/hcl2-parsing-error.md +32 -0
  2. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/CHANGELOG.md +21 -0
  3. {python_hcl2-7.0.0/python_hcl2.egg-info → python_hcl2-7.1.0}/PKG-INFO +1 -1
  4. python_hcl2-7.1.0/hcl2/builder.py +86 -0
  5. python_hcl2-7.1.0/hcl2/const.py +4 -0
  6. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/hcl2/hcl2.lark +5 -3
  7. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/hcl2/reconstructor.py +40 -36
  8. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/hcl2/transformer.py +10 -4
  9. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/hcl2/version.py +2 -2
  10. {python_hcl2-7.0.0 → python_hcl2-7.1.0/python_hcl2.egg-info}/PKG-INFO +1 -1
  11. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/python_hcl2.egg-info/SOURCES.txt +2 -0
  12. python_hcl2-7.0.0/hcl2/builder.py +0 -63
  13. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/.codacy.yml +0 -0
  14. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/.coveragerc +0 -0
  15. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/.github/CODEOWNERS +0 -0
  16. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/.github/workflows/codeql-analysis.yml +0 -0
  17. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/.github/workflows/pr_check.yml +0 -0
  18. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/.github/workflows/publish.yml +0 -0
  19. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/.gitignore +0 -0
  20. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/.pre-commit-config.yaml +0 -0
  21. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/.yamllint.yml +0 -0
  22. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/LICENSE +0 -0
  23. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/MANIFEST.in +0 -0
  24. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/README.md +0 -0
  25. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/bin/terraform_test +0 -0
  26. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/hcl2/__init__.py +0 -0
  27. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/hcl2/__main__.py +0 -0
  28. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/hcl2/api.py +0 -0
  29. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/hcl2/parser.py +0 -0
  30. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/hcl2/py.typed +0 -0
  31. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/mypy.ini +0 -0
  32. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/pylintrc +0 -0
  33. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/pyproject.toml +0 -0
  34. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/python_hcl2.egg-info/dependency_links.txt +0 -0
  35. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/python_hcl2.egg-info/entry_points.txt +0 -0
  36. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/python_hcl2.egg-info/not-zip-safe +0 -0
  37. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/python_hcl2.egg-info/requires.txt +0 -0
  38. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/python_hcl2.egg-info/top_level.txt +0 -0
  39. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/reports/.gitignore +0 -0
  40. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/requirements.txt +0 -0
  41. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/setup.cfg +0 -0
  42. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/test-requirements.txt +0 -0
  43. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/tox.ini +0 -0
  44. {python_hcl2-7.0.0 → python_hcl2-7.1.0}/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
+ ```
@@ -5,6 +5,27 @@ 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
+ ## \[Unreleased\]
9
+
10
+ - Nothing yet
11
+
12
+ ## \[7.1.0\] - 2025-04-10
13
+
14
+ ### Added
15
+
16
+ - `hcl2.builder.Builder` - nested blocks support ([#214](https://github.com/amplify-education/python-hcl2/pull/214))
17
+
18
+ ### Fixed
19
+
20
+ - Issue parsing parenthesesed identifier (reference) as an object key ([#212](https://github.com/amplify-education/python-hcl2/pull/212))
21
+ - Issue discarding empty lists when transforming python dictionary into Lark Tree ([#216](https://github.com/amplify-education/python-hcl2/pull/216))
22
+
23
+ ## \[7.0.1\] - 2025-03-31
24
+
25
+ ### Fixed
26
+
27
+ - Issue parsing dot-accessed attribute as an object key ([#209](https://github.com/amplify-education/python-hcl2/pull/209))
28
+
8
29
  ## \[7.0.0\] - 2025-03-27
9
30
 
10
31
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-hcl2
3
- Version: 7.0.0
3
+ Version: 7.1.0
4
4
  Summary: A parser for HCL2
5
5
  Author-email: Amplify Education <github@amplify.com>
6
6
  License: MIT
@@ -0,0 +1,86 @@
1
+ """A utility class for constructing HCL documents from Python code."""
2
+ from typing import List, Optional
3
+
4
+ from collections import defaultdict
5
+
6
+ from hcl2.const import START_LINE_KEY, END_LINE_KEY
7
+
8
+
9
+ class Builder:
10
+ """
11
+ The `hcl2.Builder` class produces a dictionary that should be identical to the
12
+ output of `hcl2.load(example_file, with_meta=True)`. The `with_meta` keyword
13
+ argument is important here. HCL "blocks" in the Python dictionary are
14
+ identified by the presence of `__start_line__` and `__end_line__` metadata
15
+ within them. The `Builder` class handles adding that metadata. If that metadata
16
+ is missing, the `hcl2.reconstructor.HCLReverseTransformer` class fails to
17
+ identify what is a block and what is just an attribute with an object value.
18
+ """
19
+
20
+ def __init__(self, attributes: Optional[dict] = None):
21
+ self.blocks: dict = defaultdict(list)
22
+ self.attributes = attributes or {}
23
+
24
+ def block(
25
+ self,
26
+ block_type: str,
27
+ labels: Optional[List[str]] = None,
28
+ __nested_builder__: Optional["Builder"] = None,
29
+ **attributes
30
+ ) -> "Builder":
31
+ """Create a block within this HCL document."""
32
+
33
+ if __nested_builder__ is self:
34
+ raise ValueError(
35
+ "__nested__builder__ cannot be the same instance as instance this method is called on"
36
+ )
37
+
38
+ labels = labels or []
39
+ block = Builder(attributes)
40
+
41
+ # store the block in the document
42
+ self.blocks[block_type].append((labels.copy(), block, __nested_builder__))
43
+
44
+ return block
45
+
46
+ def build(self):
47
+ """Return the Python dictionary for this HCL document."""
48
+ body = defaultdict(list)
49
+
50
+ body.update(
51
+ {
52
+ START_LINE_KEY: -1,
53
+ END_LINE_KEY: -1,
54
+ **self.attributes,
55
+ }
56
+ )
57
+
58
+ for block_type, blocks in self.blocks.items():
59
+
60
+ for labels, block_builder, nested_blocks in blocks:
61
+ # build the sub-block
62
+ block = block_builder.build()
63
+
64
+ if nested_blocks:
65
+ self._add_nested_blocks(block, nested_blocks)
66
+
67
+ # apply any labels
68
+ for label in reversed(labels):
69
+ block = {label: block}
70
+
71
+ # store it in the body
72
+ body[block_type].append(block)
73
+
74
+ return body
75
+
76
+ def _add_nested_blocks(
77
+ self, block: dict, nested_blocks_builder: "Builder"
78
+ ) -> "dict":
79
+ """Add nested blocks defined within another `Builder` instance to the `block` dictionary"""
80
+ nested_block = nested_blocks_builder.build()
81
+ for key, value in nested_block.items():
82
+ if key not in (START_LINE_KEY, END_LINE_KEY):
83
+ if key not in block.keys():
84
+ block[key] = []
85
+ block[key].extend(value)
86
+ return block
@@ -0,0 +1,4 @@
1
+ """Module for various constants used across the library"""
2
+
3
+ START_LINE_KEY = "__start_line__"
4
+ END_LINE_KEY = "__end_line__"
@@ -38,6 +38,8 @@ PLUS : "+"
38
38
  LPAR : "("
39
39
  RPAR : ")"
40
40
  COMMA : ","
41
+ DOT : "."
42
+ COLON : ":"
41
43
 
42
44
  expr_term : LPAR new_line_or_comment? expression new_line_or_comment? RPAR
43
45
  | float_lit
@@ -73,9 +75,9 @@ EQ : /[ \t]*=(?!=|>)/
73
75
 
74
76
  tuple : "[" (new_line_or_comment* expression new_line_or_comment* ",")* (new_line_or_comment* expression)? new_line_or_comment* "]"
75
77
  object : "{" new_line_or_comment? (new_line_or_comment* (object_elem | (object_elem COMMA)) new_line_or_comment*)* "}"
76
- object_elem : object_elem_key ( EQ | ":") expression
77
- object_elem_key : float_lit | int_lit | identifier | STRING_LIT
78
-
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
80
+ object_elem_key_dot_accessor : identifier (DOT identifier)+
79
81
 
80
82
  heredoc_template : /<<(?P<heredoc>[a-zA-Z][a-zA-Z0-9._-]+)\n?(?:.|\n)*?\n\s*(?P=heredoc)\n/
81
83
  heredoc_template_trim : /<<-(?P<heredoc_trim>[a-zA-Z][a-zA-Z0-9._-]+)\n?(?:.|\n)*?\n\s*(?P=heredoc_trim)\n/
@@ -10,6 +10,8 @@ from lark.lexer import Token, PatternStr, TerminalDef
10
10
  from lark.reconstruct import Reconstructor
11
11
  from lark.tree_matcher import is_discarded_terminal
12
12
  from lark.visitors import Transformer_InPlace
13
+
14
+ from hcl2.const import START_LINE_KEY, END_LINE_KEY
13
15
  from hcl2.parser import reconstruction_parser
14
16
 
15
17
 
@@ -395,46 +397,41 @@ class HCLReverseTransformer:
395
397
 
396
398
  return True
397
399
 
400
+ @classmethod
401
+ def _unwrap_interpolation(cls, value: str) -> str:
402
+ if cls._is_string_wrapped_tf(value):
403
+ return value[2:-1]
404
+ return value
405
+
398
406
  def _newline(self, level: int, count: int = 1) -> Tree:
399
407
  return Tree(
400
408
  Token("RULE", "new_line_or_comment"),
401
409
  [Token("NL_OR_COMMENT", f"\n{' ' * level}") for _ in range(count)],
402
410
  )
403
411
 
404
- # rules: the value of a block is always an array of dicts,
405
- # the key is the block type
406
- def _list_is_a_block(self, value: list) -> bool:
407
- for obj in value:
408
- if not self._dict_is_a_block(obj):
409
- return False
410
-
411
- return True
412
-
413
- def _dict_is_a_block(self, sub_obj: Any) -> bool:
414
- # if the list doesn't contain dictionaries, it's not a block
415
- if not isinstance(sub_obj, dict):
416
- return False
412
+ def _is_block(self, value: Any) -> bool:
413
+ if isinstance(value, dict):
414
+ block_body = value
415
+ if (
416
+ START_LINE_KEY in block_body.keys()
417
+ or END_LINE_KEY in block_body.keys()
418
+ ):
419
+ return True
417
420
 
418
- # if the sub object has "start_line" and "end_line" metadata,
419
- # the block itself is unlabeled, but it is a block
420
- if "__start_line__" in sub_obj.keys() or "__end_line__" in sub_obj.keys():
421
- return True
421
+ try:
422
+ # if block is labeled, actual body might be nested
423
+ # pylint: disable=W0612
424
+ block_label, block_body = next(iter(value.items()))
425
+ except StopIteration:
426
+ # no more potential labels = nothing more to check
427
+ return False
422
428
 
423
- # if the objects in the array have no metadata and more than 2 keys and
424
- # no metadata, it's just an array of objects, not a block
425
- if len(list(sub_obj)) != 1:
426
- return False
429
+ return self._is_block(block_body)
427
430
 
428
- # if the sub object has a single string key whose value is an object,
429
- # it _could_ be a labeled block... but we'd have to check if the sub
430
- # object is a block (recurse)
431
- label = list(sub_obj)[0]
432
- sub_sub_obj = sub_obj[label]
433
- if self._dict_is_a_block(sub_sub_obj):
434
- return True
431
+ if isinstance(value, list):
432
+ if len(value) > 0:
433
+ return self._is_block(value[0])
435
434
 
436
- # if the objects in the array have a single key whose child is not a
437
- # block, the array is just an array of objects, not a block
438
435
  return False
439
436
 
440
437
  def _calculate_block_labels(self, block: dict) -> Tuple[List[str], dict]:
@@ -448,8 +445,8 @@ class HCLReverseTransformer:
448
445
 
449
446
  # __start_line__ and __end_line__ metadata are not labels
450
447
  if (
451
- "__start_line__" in potential_body.keys()
452
- or "__end_line__" in potential_body.keys()
448
+ START_LINE_KEY in potential_body.keys()
449
+ or END_LINE_KEY in potential_body.keys()
453
450
  ):
454
451
  return [curr_label], potential_body
455
452
 
@@ -457,6 +454,7 @@ class HCLReverseTransformer:
457
454
  next_label, block_body = self._calculate_block_labels(potential_body)
458
455
  return [curr_label] + next_label, block_body
459
456
 
457
+ # pylint:disable=R0914
460
458
  def _transform_dict_to_body(self, hcl_dict: dict, level: int) -> Tree:
461
459
  # we add a newline at the top of a body within a block, not the root body
462
460
  # >2 here is to ignore the __start_line__ and __end_line__ metadata
@@ -467,14 +465,14 @@ class HCLReverseTransformer:
467
465
 
468
466
  # iterate through each attribute or sub-block of this block
469
467
  for key, value in hcl_dict.items():
470
- if key in ["__start_line__", "__end_line__"]:
468
+ if key in [START_LINE_KEY, END_LINE_KEY]:
471
469
  continue
472
470
 
473
471
  # construct the identifier, whether that be a block type name or an attribute key
474
472
  identifier_name = self._name_to_identifier(key)
475
473
 
476
474
  # first, check whether the value is a "block"
477
- if isinstance(value, list) and self._list_is_a_block(value):
475
+ if self._is_block(value):
478
476
  for block_v in value:
479
477
  block_labels, block_body_dict = self._calculate_block_labels(
480
478
  block_v
@@ -493,7 +491,12 @@ class HCLReverseTransformer:
493
491
  [identifier_name] + block_label_tokens + [block_body],
494
492
  )
495
493
  children.append(block)
496
- children.append(self._newline(level, count=2))
494
+ # add empty line after block
495
+ new_line = self._newline(level - 1)
496
+ # add empty line with indentation for next element in the block
497
+ new_line.children.append(self._newline(level).children[0])
498
+
499
+ children.append(new_line)
497
500
 
498
501
  # if the value isn't a block, it's an attribute
499
502
  else:
@@ -556,10 +559,11 @@ class HCLReverseTransformer:
556
559
 
557
560
  # iterate through the items and add them to the object
558
561
  for i, (k, dict_v) in enumerate(value.items()):
559
- if k in ["__start_line__", "__end_line__"]:
562
+ if k in [START_LINE_KEY, END_LINE_KEY]:
560
563
  continue
561
564
 
562
565
  value_expr_term = self._transform_value_to_expr_term(dict_v, level + 1)
566
+ k = self._unwrap_interpolation(k)
563
567
  elements.append(
564
568
  Tree(
565
569
  Token("RULE", "object_elem"),
@@ -98,15 +98,21 @@ class DictTransformer(Transformer):
98
98
  def object_elem(self, args: List) -> Dict:
99
99
  # This returns a dict with a single key/value pair to make it easier to merge these
100
100
  # into a bigger dict that is returned by the "object" function
101
- key = self.strip_quotes(str(args[0].children[0]))
102
- if len(args) == 3:
103
- value = args[2]
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]
104
106
  else:
105
- value = args[1]
107
+ key = self.strip_quotes(str(args[0].children[0]))
108
+ value = args[2]
106
109
 
107
110
  value = self.to_string_dollar(value)
108
111
  return {key: value}
109
112
 
113
+ def object_elem_key_dot_accessor(self, args: List) -> str:
114
+ return "".join(args)
115
+
110
116
  def object(self, args: List) -> Dict:
111
117
  args = self.strip_new_line_tokens(args)
112
118
  result: Dict[str, 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.0.0'
21
- __version_tuple__ = version_tuple = (7, 0, 0)
20
+ __version__ = version = '7.1.0'
21
+ __version_tuple__ = version_tuple = (7, 1, 0)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-hcl2
3
- Version: 7.0.0
3
+ Version: 7.1.0
4
4
  Summary: A parser for HCL2
5
5
  Author-email: Amplify Education <github@amplify.com>
6
6
  License: MIT
@@ -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
@@ -23,6 +24,7 @@ hcl2/__init__.py
23
24
  hcl2/__main__.py
24
25
  hcl2/api.py
25
26
  hcl2/builder.py
27
+ hcl2/const.py
26
28
  hcl2/hcl2.lark
27
29
  hcl2/parser.py
28
30
  hcl2/py.typed
@@ -1,63 +0,0 @@
1
- """A utility class for constructing HCL documents from Python code."""
2
-
3
- from typing import List, Optional
4
-
5
-
6
- class Builder:
7
- """
8
- The `hcl2.Builder` class produces a dictionary that should be identical to the
9
- output of `hcl2.load(example_file, with_meta=True)`. The `with_meta` keyword
10
- argument is important here. HCL "blocks" in the Python dictionary are
11
- identified by the presence of `__start_line__` and `__end_line__` metadata
12
- within them. The `Builder` class handles adding that metadata. If that metadata
13
- is missing, the `hcl2.reconstructor.HCLReverseTransformer` class fails to
14
- identify what is a block and what is just an attribute with an object value.
15
- """
16
-
17
- def __init__(self, attributes: Optional[dict] = None):
18
- self.blocks: dict = {}
19
- self.attributes = attributes or {}
20
-
21
- def block(
22
- self, block_type: str, labels: Optional[List[str]] = None, **attributes
23
- ) -> "Builder":
24
- """Create a block within this HCL document."""
25
- labels = labels or []
26
- block = Builder(attributes)
27
-
28
- # initialize a holder for blocks of that type
29
- if block_type not in self.blocks:
30
- self.blocks[block_type] = []
31
-
32
- # store the block in the document
33
- self.blocks[block_type].append((labels.copy(), block))
34
-
35
- return block
36
-
37
- def build(self):
38
- """Return the Python dictionary for this HCL document."""
39
- body = {
40
- "__start_line__": -1,
41
- "__end_line__": -1,
42
- **self.attributes,
43
- }
44
-
45
- for block_type, blocks in self.blocks.items():
46
-
47
- # initialize a holder for blocks of that type
48
- if block_type not in body:
49
- body[block_type] = []
50
-
51
- for labels, block_builder in blocks:
52
- # build the sub-block
53
- block = block_builder.build()
54
-
55
- # apply any labels
56
- labels.reverse()
57
- for label in labels:
58
- block = {label: block}
59
-
60
- # store it in the body
61
- body[block_type].append(block)
62
-
63
- return body
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