Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 9 additions & 13 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,17 @@ repos:
hooks:
- id: mdformat
name: Format markdown
- repo: https://github.com/psf/black
rev: 22.8.0
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.21
hooks:
- id: black
language_version: python3.8
- id: ruff-check
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.991 # Use the sha / tag you want to point at
hooks:
- id: mypy
- repo: local
hooks:
- id: pylint
name: pylint (Python Linting)
entry: pylint
language: system
types: [python]
files: ^(hcl2|test)/
args: [--rcfile=pylintrc, --output-format=colorized, --score=no]
# Pinned so the hook's own venv can build typed-ast, which has no
# wheel for Python 3.12+; this only affects mypy's own interpreter,
# not the --python-version it type-checks against.
language_version: python3.11
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,10 +193,10 @@ Always run round-trip full test suite after any modification.

## Pre-commit Checks

Hooks are defined in `.pre-commit-config.yaml` (includes black, mypy, pylint, and others). All changed files must pass these checks before committing. When writing or modifying code:
Hooks are defined in `.pre-commit-config.yaml` (includes ruff, mypy, and others). All changed files must pass these checks before committing. When writing or modifying code:

- Format Python with **black** (Python 3.8 target).
- Ensure **mypy** and **pylint** pass. Pylint config is in `pylintrc`, scoped to `hcl2/` and `test/`.
- Format and lint Python with **ruff** (config in `pyproject.toml`'s `[tool.ruff]`).
- Ensure **mypy** passes.
- End files with a newline; strip trailing whitespace (except under `test/integration/(hcl2_reconstructed|specialized)/`).

## Keeping Docs Current
Expand Down
7 changes: 5 additions & 2 deletions bin/check_deps.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
"""Used by dependencies_check.yml to verify if dependencies between pyproject.yml and requirements.txt are in sync"""
"""Used by dependencies_check.yml to verify if dependencies between pyproject.yml and
requirements.txt are in sync"""

import difflib
import sys
from typing import Set
import difflib

import tomli


Expand Down
12 changes: 5 additions & 7 deletions bin/terraform_test
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ Options:
PATH The directory to check. Defaults to the TERRAFORM_CONFIG environment variable

"""

import argparse
import os

from hcl2 import load
from hcl2.version import __version__

from hcl2 import load

if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="This script recursively converts hcl2 files to json"
)
parser = argparse.ArgumentParser(description="This script recursively converts hcl2 files to json")
parser.add_argument("PATH", nargs="?", default=None, help="The path to convert")
parser.add_argument("--version", action="version", version=__version__)

Expand All @@ -27,9 +27,7 @@ if __name__ == "__main__":
target_dir = args.PATH if args.PATH else os.environ["TERRAFORM_CONFIG"]
for curr_dir, dirs, files in os.walk(target_dir):
for file_name in files:
if ".terraform" not in curr_dir and (
file_name.endswith(".tf") or file_name.endswith("tfvars")
):
if ".terraform" not in curr_dir and (file_name.endswith(".tf") or file_name.endswith("tfvars")):
file_path = os.path.join(curr_dir, file_name)

with open(file_path, "r") as file:
Expand Down
25 changes: 8 additions & 17 deletions cli/hcl_to_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@
import sys
from typing import IO, List, Optional, TextIO

from hcl2 import load
from hcl2.utils import SerializationOptions
from hcl2.version import __version__

from cli.helpers import (
EXIT_IO_ERROR,
EXIT_PARSE_ERROR,
Expand All @@ -23,6 +22,8 @@
_expand_file_args,
_install_sigpipe_handler,
)
from hcl2 import load
from hcl2.utils import SerializationOptions

_HCL_EXTENSIONS = {".tf", ".hcl"}

Expand Down Expand Up @@ -125,22 +126,16 @@ def _stream_ndjson( # pylint: disable=too-many-arguments,too-many-positional-ar
print(file_path, file=sys.stderr, flush=True)
try:
if file_path == "-":
data = _load_to_dict(
sys.stdin, options, only=only, exclude=exclude, fields=fields
)
data = _load_to_dict(sys.stdin, options, only=only, exclude=exclude, fields=fields)
else:
with open(file_path, "r", encoding="utf-8") as f:
data = _load_to_dict(
f, options, only=only, exclude=exclude, fields=fields
)
data = _load_to_dict(f, options, only=only, exclude=exclude, fields=fields)
except HCL_SKIPPABLE as exc:
if skip:
worst_exit = max(worst_exit, EXIT_PARTIAL)
continue
print(
_error(
str(exc), use_json=True, error_type="parse_error", file=file_path
),
_error(str(exc), use_json=True, error_type="parse_error", file=file_path),
file=sys.stderr,
)
return EXIT_PARSE_ERROR
Expand Down Expand Up @@ -197,9 +192,7 @@ def main(): # pylint: disable=too-many-branches,too-many-statements,too-many-lo
epilog=_EXAMPLES,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"-s", dest="skip", action="store_true", help="Skip un-parsable files"
)
parser.add_argument("-s", dest="skip", action="store_true", help="Skip un-parsable files")
parser.add_argument(
"PATH",
nargs="*",
Expand Down Expand Up @@ -382,9 +375,7 @@ def convert(in_file, out_file):
if len(paths) == 1:
path = paths[0]
if path == "-" or os.path.isfile(path):
if not _convert_single_file(
path, output, convert, args.skip, HCL_SKIPPABLE, quiet=quiet
):
if not _convert_single_file(path, output, convert, args.skip, HCL_SKIPPABLE, quiet=quiet):
sys.exit(EXIT_PARTIAL)
elif os.path.isdir(path):
if output is None:
Expand Down
10 changes: 3 additions & 7 deletions cli/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import signal
import sys
from io import StringIO
from typing import Callable, IO, List, Optional, Set, Tuple, Type
from typing import IO, Callable, List, Optional, Set, Tuple, Type

from lark import UnexpectedCharacters, UnexpectedToken

Expand Down Expand Up @@ -158,9 +158,7 @@ def _convert_directory( # pylint: disable=too-many-positional-arguments,too-man
for current_dir, _, files in os.walk(in_path):
dir_prefix = os.path.commonpath([in_path, current_dir])
relative_current_dir = os.path.relpath(current_dir, dir_prefix)
current_out_path = os.path.normpath(
os.path.join(out_path, relative_current_dir)
)
current_out_path = os.path.normpath(os.path.join(out_path, relative_current_dir))
if not os.path.exists(current_out_path):
os.makedirs(current_out_path)
for file_name in files:
Expand Down Expand Up @@ -226,9 +224,7 @@ def _convert_multiple_files( # pylint: disable=too-many-positional-arguments
file_out_dir = os.path.dirname(file_out)
if file_out_dir and not os.path.exists(file_out_dir):
os.makedirs(file_out_dir)
if not _convert_single_file(
in_path, file_out, convert_fn, skip, skippable, quiet=quiet
):
if not _convert_single_file(in_path, file_out, convert_fn, skip, skippable, quiet=quiet):
any_skipped = True
return any_skipped

Expand Down
67 changes: 19 additions & 48 deletions cli/hq.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,21 @@
import sys
from typing import Any, List, Optional, Tuple

from hcl2.version import __version__

from hcl2.query._base import NodeView
from hcl2.utils import SerializationOptions
from hcl2.query.body import DocumentView
from hcl2.query.introspect import build_schema, describe_results
from hcl2.query.path import QuerySyntaxError
from hcl2.query.pipeline import classify_stage, execute_pipeline, split_pipeline
from hcl2.query.resolver import resolve_path
from hcl2.query.safe_eval import (
UnsafeExpressionError,
_SAFE_CALLABLE_NAMES,
UnsafeExpressionError,
safe_eval,
)
from hcl2.version import __version__
from hcl2.utils import SerializationOptions

from .helpers import _expand_file_args # noqa: F401 — re-exported for tests

# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -171,9 +173,7 @@ def _collect_files(path: str) -> List[str]:
def _error(msg: str, use_json: bool, **extra) -> str:
"""Format an error message."""
if use_json:
return json.dumps(
{"error": extra.get("error_type", "error"), "message": msg, **extra}
)
return json.dumps({"error": extra.get("error_type", "error"), "message": msg, **extra})
return f"Error: {msg}"


Expand Down Expand Up @@ -347,17 +347,12 @@ def format_result(self, result: Any) -> str:
def format_list(self, items: list) -> str:
"""Format a list result (e.g. from hybrid mode returning a list)."""
if self.output_json:
converted = [
_convert_for_json(item, options=self.serialization_options)
for item in items
]
converted = [_convert_for_json(item, options=self.serialization_options) for item in items]
return json.dumps(converted, indent=self.json_indent, default=str)
parts = []
for item in items:
if isinstance(item, NodeView):
parts.append(
item.to_hcl() if not self.output_value else str(item.to_dict())
)
parts.append(item.to_hcl() if not self.output_value else str(item.to_dict()))
else:
parts.append(str(item))
if not self.output_value:
Expand All @@ -367,10 +362,7 @@ def format_list(self, items: list) -> str:
def format_output(self, results: List[Any]) -> str:
"""Format results for final output."""
if self.output_json and len(results) > 1:
items = [
_convert_for_json(item, options=self.serialization_options)
for item in results
]
items = [_convert_for_json(item, options=self.serialization_options) for item in results]
return json.dumps(items, indent=self.json_indent, default=str)
return "\n".join(self.format_result(r) for r in results)

Expand Down Expand Up @@ -427,9 +419,7 @@ def emit(self, results: List[Any], file_path: str) -> None:

# JSON + multi — accumulate for merged output
if self.config.output_json and self.multi:
self._accumulator.extend(
_convert_results(results, file_path, self.multi, self.config)
)
self._accumulator.extend(_convert_results(results, file_path, self.multi, self.config))
return

# Single-file output (with_location or default)
Expand Down Expand Up @@ -458,12 +448,8 @@ def flush(self) -> None:
"""Sort and emit accumulated JSON results."""
if not self._accumulator:
return
self._accumulator.sort(
key=lambda x: x.get("__file__", "") if isinstance(x, dict) else ""
)
print(
json.dumps(self._accumulator, indent=self.config.json_indent, default=str)
)
self._accumulator.sort(key=lambda x: x.get("__file__", "") if isinstance(x, dict) else "")
print(json.dumps(self._accumulator, indent=self.config.json_indent, default=str))
self._accumulator.clear()


Expand Down Expand Up @@ -543,9 +529,7 @@ def _process_file(args_tuple):
return (file_path, EXIT_SUCCESS, converted, None)


def _run_diff(
file1: str, file2: str, use_json: bool, json_indent: Optional[int]
) -> int:
def _run_diff(file1: str, file2: str, use_json: bool, json_indent: Optional[int]) -> int:
"""Run structural diff between two HCL files.

Returns an exit code: 0 if files are identical, 1 if they differ.
Expand All @@ -554,9 +538,7 @@ def _run_diff(
import hcl2
from hcl2.query.diff import diff_dicts, format_diff_json, format_diff_text

opts = SerializationOptions(
with_comments=False, with_meta=False, explicit_blocks=True
)
opts = SerializationOptions(with_comments=False, with_meta=False, explicit_blocks=True)
for path in (file1, file2):
if path == "-":
continue
Expand Down Expand Up @@ -630,9 +612,7 @@ def _build_parser() -> argparse.ArgumentParser:

output_group = parser.add_mutually_exclusive_group()
output_group.add_argument("--json", action="store_true", help="Output as JSON")
output_group.add_argument(
"--value", action="store_true", help="Output raw value only"
)
output_group.add_argument("--value", action="store_true", help="Output raw value only")
output_group.add_argument(
"--raw",
action="store_true",
Expand Down Expand Up @@ -796,9 +776,7 @@ def _execute_and_emit(
output_config: OutputConfig,
) -> int:
"""Execute queries across files and emit results. Returns an exit code."""
file_paths = [
fp for fa in _expand_file_args(args.FILE) for fp in _collect_files(fa)
]
file_paths = [fp for fa in _expand_file_args(args.FILE) for fp in _collect_files(fa)]
any_results = False
worst_exit = EXIT_SUCCESS
multi = len(file_paths) > 1
Expand All @@ -816,14 +794,9 @@ def _execute_and_emit(
with OutputSink(output_config, multi) as sink:
if use_parallel:
n_workers = args.jobs or min(os.cpu_count() or 1, len(file_paths))
worker_args = [
(fp, query, False, args.QUERY, multi, output_config)
for fp in file_paths
]
worker_args = [(fp, query, False, args.QUERY, multi, output_config) for fp in file_paths]
with multiprocessing.Pool(n_workers) as pool:
for fp, exit_code, converted, error_msg in pool.imap_unordered(
_process_file, worker_args
):
for fp, exit_code, converted, error_msg in pool.imap_unordered(_process_file, worker_args):
if error_msg:
etype = _EXIT_TO_ERROR_TYPE.get(exit_code, "error")
print(
Expand All @@ -838,9 +811,7 @@ def _execute_and_emit(
sink.emit_converted(converted)
else:
for file_path in file_paths:
results, exit_code = _run_query_on_file(
file_path, query, args.eval, use_json, args.QUERY
)
results, exit_code = _run_query_on_file(file_path, query, args.eval, use_json, args.QUERY)
if results is None:
worst_exit = max(worst_exit, exit_code)
continue
Expand Down
Loading
Loading