Skip to content

Commit 7662a5e

Browse files
rewrite api.py, update builder.py, add unit tests for them
1 parent 4054fc9 commit 7662a5e

7 files changed

Lines changed: 607 additions & 51 deletions

File tree

hcl2/__init__.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,21 @@
88
from .api import (
99
load,
1010
loads,
11+
dump,
12+
dumps,
1113
parse,
1214
parses,
15+
parse_to_tree,
16+
parses_to_tree,
17+
from_dict,
18+
from_json,
19+
reconstruct,
1320
transform,
14-
writes,
21+
serialize,
1522
)
1623

1724
from .builder import Builder
25+
from .deserializer import DeserializerOptions
26+
from .formatter import FormatterOptions
27+
from .rules.base import StartRule
28+
from .utils import SerializationOptions

hcl2/__main__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from lark import UnexpectedCharacters, UnexpectedToken
2020

2121
from . import load
22+
from .utils import SerializationOptions
2223
from .version import __version__
2324

2425

@@ -58,7 +59,8 @@ def main():
5859
else open(args.OUT_PATH, "w", encoding="utf-8")
5960
)
6061
print(args.PATH, file=sys.stderr, flush=True)
61-
json.dump(load(in_file, with_meta=args.with_meta), out_file)
62+
options = SerializationOptions(with_meta=True) if args.with_meta else None
63+
json.dump(load(in_file, serialization_options=options), out_file)
6264
if args.OUT_PATH is None:
6365
out_file.write("\n")
6466
out_file.close()

hcl2/api.py

Lines changed: 182 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,205 @@
1-
"""The API that will be exposed to users of this package"""
2-
from typing import TextIO
1+
"""The API that will be exposed to users of this package.
2+
3+
Follows the json module convention: load/loads for reading, dump/dumps for writing.
4+
Also exposes intermediate pipeline stages for advanced usage.
5+
"""
6+
7+
import json as _json
8+
from typing import TextIO, Optional
39

410
from lark.tree import Tree
5-
from hcl2.parser import parser
11+
12+
from hcl2.deserializer import BaseDeserializer, DeserializerOptions
13+
from hcl2.formatter import BaseFormatter, FormatterOptions
14+
from hcl2.parser import parser as _get_parser
615
from hcl2.reconstructor import HCLReconstructor
16+
from hcl2.rules.base import StartRule
717
from hcl2.transformer import RuleTransformer
18+
from hcl2.utils import SerializationOptions
19+
20+
21+
# ---------------------------------------------------------------------------
22+
# Primary API: load / loads / dump / dumps
23+
# ---------------------------------------------------------------------------
24+
25+
26+
def load(
27+
file: TextIO,
28+
*,
29+
serialization_options: Optional[SerializationOptions] = None,
30+
) -> dict:
31+
"""Load a HCL2 file and return a Python dict.
32+
33+
:param file: File with HCL2 content.
34+
:param serialization_options: Options controlling serialization behavior.
35+
"""
36+
return loads(file.read(), serialization_options=serialization_options)
37+
38+
39+
def loads(
40+
text: str,
41+
*,
42+
serialization_options: Optional[SerializationOptions] = None,
43+
) -> dict:
44+
"""Load HCL2 from a string and return a Python dict.
845
46+
:param text: HCL2 text.
47+
:param serialization_options: Options controlling serialization behavior.
48+
"""
49+
tree = parses(text)
50+
return serialize(tree, serialization_options=serialization_options)
51+
52+
53+
def dump(
54+
data: dict,
55+
file: TextIO,
56+
*,
57+
deserializer_options: Optional[DeserializerOptions] = None,
58+
formatter_options: Optional[FormatterOptions] = None,
59+
) -> None:
60+
"""Write a Python dict as HCL2 to a file.
961
10-
def load(file: TextIO, with_meta=False) -> dict:
11-
"""Load a HCL2 file.
12-
:param file: File with hcl2 to be loaded as a dict.
13-
:param with_meta: If set to true then adds `__start_line__` and `__end_line__`
14-
parameters to the output dict. Default to false.
62+
:param data: Python dict (as produced by :func:`load`).
63+
:param file: Writable text file.
64+
:param deserializer_options: Options controlling deserialization behavior.
65+
:param formatter_options: Options controlling formatting behavior.
1566
"""
16-
return loads(file.read(), with_meta=with_meta)
67+
file.write(dumps(data, deserializer_options=deserializer_options, formatter_options=formatter_options))
1768

1869

19-
def loads(text: str, with_meta=False) -> dict:
20-
"""Load HCL2 from a string.
21-
:param text: Text with hcl2 to be loaded as a dict.
22-
:param with_meta: If set to true then adds `__start_line__` and `__end_line__`
23-
parameters to the output dict. Default to false.
70+
def dumps(
71+
data: dict,
72+
*,
73+
deserializer_options: Optional[DeserializerOptions] = None,
74+
formatter_options: Optional[FormatterOptions] = None,
75+
) -> str:
76+
"""Convert a Python dict to an HCL2 string.
77+
78+
:param data: Python dict (as produced by :func:`load`).
79+
:param deserializer_options: Options controlling deserialization behavior.
80+
:param formatter_options: Options controlling formatting behavior.
2481
"""
25-
# append new line as a workaround for https://github.com/lark-parser/lark/issues/237
82+
tree = from_dict(data, deserializer_options=deserializer_options, formatter_options=formatter_options)
83+
return reconstruct(tree)
84+
85+
86+
# ---------------------------------------------------------------------------
87+
# Parsing: HCL text -> LarkElement tree or raw Lark tree
88+
# ---------------------------------------------------------------------------
89+
90+
91+
def parse(file: TextIO, *, discard_comments: bool = False) -> StartRule:
92+
"""Parse a HCL2 file into a LarkElement tree.
93+
94+
:param file: File with HCL2 content.
95+
:param discard_comments: If True, discard comments during transformation.
96+
"""
97+
return parses(file.read(), discard_comments=discard_comments)
98+
99+
100+
def parses(text: str, *, discard_comments: bool = False) -> StartRule:
101+
"""Parse a HCL2 string into a LarkElement tree.
102+
103+
:param text: HCL2 text.
104+
:param discard_comments: If True, discard comments during transformation.
105+
"""
106+
lark_tree = parses_to_tree(text)
107+
return transform(lark_tree, discard_comments=discard_comments)
108+
109+
110+
def parse_to_tree(file: TextIO) -> Tree:
111+
"""Parse a HCL2 file into a raw Lark parse tree.
112+
113+
:param file: File with HCL2 content.
114+
"""
115+
return parses_to_tree(file.read())
116+
117+
118+
def parses_to_tree(text: str) -> Tree:
119+
"""Parse a HCL2 string into a raw Lark parse tree.
120+
121+
:param text: HCL2 text.
122+
"""
123+
# Append newline as workaround for https://github.com/lark-parser/lark/issues/237
26124
# Lark doesn't support EOF token so our grammar can't look for "new line or end of file"
27-
# This means that all blocks must end in a new line even if the file ends
28-
# Append a new line as a temporary fix
29-
tree = parser().parse(text + "\n")
30-
return RuleTransformer().transform(tree)
125+
return _get_parser().parse(text + "\n")
31126

32127

33-
def parse(file: TextIO) -> Tree:
34-
"""Load HCL2 syntax tree from a file.
35-
:param file: File with hcl2 to be loaded as a dict.
128+
# ---------------------------------------------------------------------------
129+
# Intermediate pipeline stages
130+
# ---------------------------------------------------------------------------
131+
132+
133+
def from_dict(
134+
data: dict,
135+
*,
136+
deserializer_options: Optional[DeserializerOptions] = None,
137+
formatter_options: Optional[FormatterOptions] = None,
138+
format: bool = True,
139+
) -> StartRule:
140+
"""Convert a Python dict into a LarkElement tree.
141+
142+
:param data: Python dict (as produced by :func:`load`).
143+
:param deserializer_options: Options controlling deserialization behavior.
144+
:param formatter_options: Options controlling formatting behavior.
145+
:param format: If True (default), apply formatting to the tree.
146+
"""
147+
deserializer = BaseDeserializer(deserializer_options)
148+
tree = deserializer.load_python(data)
149+
if format:
150+
formatter = BaseFormatter(formatter_options)
151+
formatter.format_tree(tree)
152+
return tree
153+
154+
155+
def from_json(
156+
text: str,
157+
*,
158+
deserializer_options: Optional[DeserializerOptions] = None,
159+
formatter_options: Optional[FormatterOptions] = None,
160+
format: bool = True,
161+
) -> StartRule:
162+
"""Convert a JSON string into a LarkElement tree.
163+
164+
:param text: JSON string.
165+
:param deserializer_options: Options controlling deserialization behavior.
166+
:param formatter_options: Options controlling formatting behavior.
167+
:param format: If True (default), apply formatting to the tree.
36168
"""
37-
return parses(file.read())
169+
data = _json.loads(text)
170+
return from_dict(data, deserializer_options=deserializer_options, formatter_options=formatter_options, format=format)
171+
38172

173+
def reconstruct(tree) -> str:
174+
"""Convert a LarkElement tree (or raw Lark tree) to an HCL2 string.
39175
40-
def parses(text: str) -> Tree:
41-
"""Load HCL2 syntax tree from a string.
42-
:param text: Text with hcl2 to be loaded as a dict.
176+
:param tree: A :class:`StartRule` (LarkElement tree) or :class:`lark.Tree`.
43177
"""
44-
return parser().parse(text)
178+
reconstructor = HCLReconstructor()
179+
if isinstance(tree, StartRule):
180+
tree = tree.to_lark()
181+
return reconstructor.reconstruct(tree)
45182

46183

47-
def transform(ast: Tree, with_meta=False) -> dict:
48-
"""Convert an HCL2 AST to a dictionary.
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.
184+
def transform(lark_tree: Tree, *, discard_comments: bool = False) -> StartRule:
185+
"""Transform a raw Lark parse tree into a LarkElement tree.
186+
187+
:param lark_tree: Raw Lark tree from :func:`parse_to_tree` or :func:`parse_string_to_tree`.
188+
:param discard_comments: If True, discard comments during transformation.
52189
"""
53-
return RuleTransformer().transform(ast)
190+
return RuleTransformer(discard_new_line_or_comments=discard_comments).transform(lark_tree)
191+
54192

193+
def serialize(
194+
tree: StartRule,
195+
*,
196+
serialization_options: Optional[SerializationOptions] = None,
197+
) -> dict:
198+
"""Serialize a LarkElement tree to a Python dict.
55199
56-
def writes(ast: Tree) -> str:
57-
"""Convert an HCL2 syntax tree to a string.
58-
:param ast: HCL2 syntax tree, output from `parse` or `parses`
200+
:param tree: A :class:`StartRule` (LarkElement tree).
201+
:param serialization_options: Options controlling serialization behavior.
59202
"""
60-
return HCLReconstructor().reconstruct(ast)
203+
if serialization_options is not None:
204+
return tree.serialize(options=serialization_options)
205+
return tree.serialize()

hcl2/builder.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,16 @@
33

44
from collections import defaultdict
55

6-
from hcl2.const import START_LINE_KEY, END_LINE_KEY
6+
from hcl2.const import IS_BLOCK
77

88

99
class Builder:
1010
"""
1111
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.
12+
output of `hcl2.load(example_file)`. HCL "blocks" in the Python dictionary are
13+
identified by the presence of `__is_block__: True` markers within them.
14+
The `Builder` class handles adding that marker. If that marker is missing,
15+
the deserializer fails to distinguish blocks from regular object attributes.
1816
"""
1917

2018
def __init__(self, attributes: Optional[dict] = None):
@@ -49,8 +47,7 @@ def build(self):
4947

5048
body.update(
5149
{
52-
START_LINE_KEY: -1,
53-
END_LINE_KEY: -1,
50+
IS_BLOCK: True,
5451
**self.attributes,
5552
}
5653
)
@@ -79,7 +76,7 @@ def _add_nested_blocks(
7976
"""Add nested blocks defined within another `Builder` instance to the `block` dictionary"""
8077
nested_block = nested_blocks_builder.build()
8178
for key, value in nested_block.items():
82-
if key not in (START_LINE_KEY, END_LINE_KEY):
79+
if key != IS_BLOCK:
8380
if key not in block.keys():
8481
block[key] = []
8582
block[key].extend(value)

hcl2/deserializer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from regex import regex
88

9-
from hcl2 import parses
9+
from hcl2.parser import parser as _get_parser
1010
from hcl2.const import IS_BLOCK
1111
from hcl2.rules.abstract import LarkElement, LarkRule
1212
from hcl2.rules.base import (
@@ -217,7 +217,7 @@ def _deserialize_expression(self, value: str) -> ExprTermRule:
217217
# create HCL2 snippet
218218
value = f"temp = {value}"
219219
# parse the above
220-
parsed_tree = parses(value)
220+
parsed_tree = _get_parser().parse(value)
221221
# transform parsed tree into LarkElement tree
222222
rules_tree = self._transformer.transform(parsed_tree)
223223
# extract expression from the tree

0 commit comments

Comments
 (0)