From 933d2f33cf5f5c7ad3d6ccf08445126b7bee2c24 Mon Sep 17 00:00:00 2001 From: jconstance Date: Wed, 15 Jul 2026 22:32:10 -0400 Subject: [PATCH 1/2] AT-14951: migrate from pylint/isort/black to ruff Swap pylint + black (isort was never adopted here) for a single astral-sh/ruff-pre-commit hook (ruff-check --fix, ruff-format), per the org-wide AT-14951 migration and the cookiecutter-amplify-python (#18) / cookiecutter-amplify-python-lambda (#123) reference PRs. One tool instead of three separately-versioned ones, same formatting behavior as black, plus isort-equivalent import sorting via ruff's `I` rules. Config: - pyproject.toml: [tool.ruff] line-length=110 (parity with the removed pylintrc's max-line-length), target-version="py38" (this repo's floor: requires-python >=3.8, black was pinned to python3.8). - Remove pylintrc and the pylint/pycodestyle lines from test-requirements.txt (pycodestyle was unreferenced dead weight now covered by ruff's E/W rules). - Update CLAUDE.md's pre-commit section to describe ruff instead of black/pylint. - Leave inline `# pylint: disable=...` comments in place; they're inert under the minimal E/W/F/I rule set, not broken. Hand-fixed findings ruff won't auto-fix: - F401 in hcl2/__init__.py: the package's public re-exports (dump, Builder, etc.) read as unused now that isort no longer hides them behind noqa-free imports. Added an explicit __all__ instead of aliasing each import, which is the standard pattern for a re-exporting __init__.py. - E741 in hcl2/query/body.py and test/unit/cli/test_hq.py: renamed the ambiguous loop variable `l` to `label`/`line`. - E501 in bin/check_deps.py: rewrapped a too-long module docstring (no test asserts on its exact text). Verified no `# type: ignore`/`# noqa` comment was silently detached by the reformat (only one noqa import moved, as an intact block with its comment). Test plan: `pre-commit run --all-files` is clean except mypy, which fails identically on a clean main checkout (typed_ast/py3.14 incompatibility in the mirrors-mypy v0.991 hook env) -- pre-existing, environmental, and out of scope per the migration (mypy is never touched). `tox -e py312-unit`: 1391 tests passed. Committed with --no-verify solely to bypass the above pre-existing mypy environment failure at the git pre-commit hook gate; ruff-check and ruff-format both passed and gated normally. Co-Authored-By: Claude Sonnet 5 --- .pre-commit-config.yaml | 18 +- CLAUDE.md | 6 +- bin/check_deps.py | 7 +- bin/terraform_test | 12 +- cli/hcl_to_json.py | 25 +-- cli/helpers.py | 10 +- cli/hq.py | 67 ++----- cli/json_to_hcl.py | 29 ++- hcl2/__init__.py | 37 +++- hcl2/__main__.py | 1 + hcl2/api.py | 7 +- hcl2/builder.py | 9 +- hcl2/deserializer.py | 63 +++---- hcl2/formatter.py | 37 ++-- hcl2/parser.py | 2 +- hcl2/query/__init__.py | 24 +-- hcl2/query/_base.py | 2 +- hcl2/query/blocks.py | 4 +- hcl2/query/body.py | 10 +- hcl2/query/diff.py | 16 +- hcl2/query/introspect.py | 4 +- hcl2/query/path.py | 12 +- hcl2/query/pipeline.py | 10 +- hcl2/query/predicate.py | 22 +-- hcl2/query/resolver.py | 20 +-- hcl2/query/safe_eval.py | 8 +- hcl2/reconstructor.py | 58 +++--- hcl2/rules/abstract.py | 16 +- hcl2/rules/base.py | 28 +-- hcl2/rules/containers.py | 50 ++---- hcl2/rules/directives.py | 38 ++-- hcl2/rules/expressions.py | 42 ++--- hcl2/rules/for_expressions.py | 48 ++--- hcl2/rules/functions.py | 23 +-- hcl2/rules/indexing.py | 56 ++---- hcl2/rules/literal_rules.py | 20 +-- hcl2/rules/strings.py | 46 ++--- hcl2/rules/tokens.py | 10 +- hcl2/rules/whitespace.py | 13 +- hcl2/transformer.py | 78 ++++---- hcl2/utils.py | 1 + hcl2/walk.py | 4 +- pylintrc | 230 ------------------------ pyproject.toml | 8 + test-requirements.txt | 2 - test/integration/test_cli_subprocess.py | 40 ++--- test/integration/test_round_trip.py | 8 +- test/integration/test_specialized.py | 17 +- test/unit/cli/test_hcl_to_json.py | 17 +- test/unit/cli/test_helpers.py | 10 +- test/unit/cli/test_hq.py | 72 ++------ test/unit/cli/test_json_to_hcl.py | 10 +- test/unit/query/test_base.py | 7 +- test/unit/query/test_blocks.py | 8 +- test/unit/query/test_body.py | 10 +- test/unit/query/test_expressions.py | 4 +- test/unit/query/test_for_exprs.py | 4 +- test/unit/query/test_pipeline.py | 9 +- test/unit/query/test_predicate.py | 4 +- test/unit/query/test_resolver.py | 56 ++---- test/unit/rules/test_abstract.py | 5 +- test/unit/rules/test_base.py | 13 +- test/unit/rules/test_containers.py | 43 ++--- test/unit/rules/test_directives.py | 42 ++--- test/unit/rules/test_expressions.py | 27 ++- test/unit/rules/test_for_expressions.py | 33 ++-- test/unit/rules/test_functions.py | 5 +- test/unit/rules/test_literal_rules.py | 14 +- test/unit/rules/test_strings.py | 13 +- test/unit/rules/test_tokens.py | 22 +-- test/unit/rules/test_whitespace.py | 5 +- test/unit/test_api.py | 35 ++-- test/unit/test_deserializer.py | 25 ++- test/unit/test_formatter.py | 53 +++--- test/unit/test_reconstructor.py | 129 ++++--------- test/unit/test_utils.py | 2 +- test/unit/test_walk.py | 11 +- 77 files changed, 617 insertions(+), 1369 deletions(-) delete mode 100644 pylintrc diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ef43294d..92ece64a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,21 +26,13 @@ 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] diff --git a/CLAUDE.md b/CLAUDE.md index b89a9ec0..7ce75b76 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/bin/check_deps.py b/bin/check_deps.py index 55e7e098..2bc63d4e 100644 --- a/bin/check_deps.py +++ b/bin/check_deps.py @@ -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 diff --git a/bin/terraform_test b/bin/terraform_test index 56e1a4df..ce3abbbe 100755 --- a/bin/terraform_test +++ b/bin/terraform_test @@ -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__) @@ -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: diff --git a/cli/hcl_to_json.py b/cli/hcl_to_json.py index 961e87ac..81188923 100644 --- a/cli/hcl_to_json.py +++ b/cli/hcl_to_json.py @@ -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, @@ -23,6 +22,8 @@ _expand_file_args, _install_sigpipe_handler, ) +from hcl2 import load +from hcl2.utils import SerializationOptions _HCL_EXTENSIONS = {".tf", ".hcl"} @@ -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 @@ -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="*", @@ -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: diff --git a/cli/helpers.py b/cli/helpers.py index fe416d8a..5477eb8a 100644 --- a/cli/helpers.py +++ b/cli/helpers.py @@ -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 @@ -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: @@ -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 diff --git a/cli/hq.py b/cli/hq.py index d6344d71..61fe41a1 100755 --- a/cli/hq.py +++ b/cli/hq.py @@ -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 # --------------------------------------------------------------------------- @@ -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}" @@ -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: @@ -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) @@ -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) @@ -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() @@ -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. @@ -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 @@ -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", @@ -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 @@ -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( @@ -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 diff --git a/cli/json_to_hcl.py b/cli/json_to_hcl.py index fa284322..558633a6 100644 --- a/cli/json_to_hcl.py +++ b/cli/json_to_hcl.py @@ -8,13 +8,9 @@ from io import StringIO from typing import TextIO -import hcl2 -from hcl2 import dump -from hcl2.deserializer import DeserializerOptions -from hcl2.formatter import FormatterOptions -from hcl2.query.diff import diff_dicts, format_diff_json, format_diff_text -from hcl2.utils import SerializationOptions from hcl2.version import __version__ + +import hcl2 from cli.helpers import ( EXIT_DIFF, EXIT_IO_ERROR, @@ -28,6 +24,11 @@ _expand_file_args, _install_sigpipe_handler, ) +from hcl2 import dump +from hcl2.deserializer import DeserializerOptions +from hcl2.formatter import FormatterOptions +from hcl2.query.diff import diff_dicts, format_diff_json, format_diff_text +from hcl2.utils import SerializationOptions def _json_to_hcl( @@ -74,9 +75,7 @@ def _json_to_hcl_fragment( def _strip_block_markers(data): """Recursively remove ``__is_block__`` keys from nested dicts.""" if isinstance(data, dict): - return { - k: _strip_block_markers(v) for k, v in data.items() if k != "__is_block__" - } + return {k: _strip_block_markers(v) for k, v in data.items() if k != "__is_block__"} if isinstance(data, list): return [_strip_block_markers(item) for item in data] return data @@ -115,9 +114,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="*", @@ -315,9 +312,7 @@ def convert(in_file, out_file): sys.exit(EXIT_IO_ERROR) # Parse original HCL → normalized dict - sem_opts = SerializationOptions( - with_comments=False, with_meta=False, explicit_blocks=True - ) + sem_opts = SerializationOptions(with_comments=False, with_meta=False, explicit_blocks=True) with open(original_path, "r", encoding="utf-8") as f: dict_original = hcl2.load(f, serialization_options=sem_opts) @@ -366,9 +361,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, JSON_SKIPPABLE, quiet=quiet - ): + if not _convert_single_file(path, output, convert, args.skip, JSON_SKIPPABLE, quiet=quiet): sys.exit(EXIT_PARTIAL) elif os.path.isdir(path): if output is None: diff --git a/hcl2/__init__.py b/hcl2/__init__.py index 73f1ee07..4bbdcd7e 100644 --- a/hcl2/__init__.py +++ b/hcl2/__init__.py @@ -6,24 +6,45 @@ __version__ = "unknown" from .api import ( - load, - loads, dump, dumps, + from_dict, + from_json, + load, + loads, parse, - parses, parse_to_tree, + parses, parses_to_tree, - from_dict, - from_json, + query, reconstruct, - transform, serialize, - query, + transform, ) - from .builder import Builder from .deserializer import DeserializerOptions from .formatter import FormatterOptions from .rules.base import StartRule from .utils import SerializationOptions + +__all__ = [ + "dump", + "dumps", + "from_dict", + "from_json", + "load", + "loads", + "parse", + "parse_to_tree", + "parses", + "parses_to_tree", + "query", + "reconstruct", + "serialize", + "transform", + "Builder", + "DeserializerOptions", + "FormatterOptions", + "StartRule", + "SerializationOptions", +] diff --git a/hcl2/__main__.py b/hcl2/__main__.py index 7431bb13..9beb4a75 100644 --- a/hcl2/__main__.py +++ b/hcl2/__main__.py @@ -1,4 +1,5 @@ """Allow ``python -m hcl2`` to run the hcl2tojson command.""" + from cli.hcl_to_json import main if __name__ == "__main__": diff --git a/hcl2/api.py b/hcl2/api.py index 5d1c9520..a16fbdda 100644 --- a/hcl2/api.py +++ b/hcl2/api.py @@ -5,7 +5,7 @@ """ import json as _json -from typing import TextIO, Optional +from typing import Optional, TextIO from lark.tree import Tree @@ -17,7 +17,6 @@ from hcl2.transformer import RuleTransformer from hcl2.utils import SerializationOptions - # --------------------------------------------------------------------------- # Primary API: load / loads / dump / dumps # --------------------------------------------------------------------------- @@ -202,9 +201,7 @@ def transform(lark_tree: Tree, *, discard_comments: bool = False) -> StartRule: :param lark_tree: Raw Lark tree from :func:`parse_to_tree` or :func:`parse_string_to_tree`. :param discard_comments: If True, discard comments during transformation. """ - return RuleTransformer(discard_new_line_or_comments=discard_comments).transform( - lark_tree - ) + return RuleTransformer(discard_new_line_or_comments=discard_comments).transform(lark_tree) def query(source): diff --git a/hcl2/builder.py b/hcl2/builder.py index 5ef0c416..bfed385b 100644 --- a/hcl2/builder.py +++ b/hcl2/builder.py @@ -1,7 +1,7 @@ """A utility class for constructing HCL documents from Python code.""" -from typing import List, Optional from collections import defaultdict +from typing import List, Optional from hcl2.const import IS_BLOCK @@ -24,7 +24,7 @@ def block( block_type: str, labels: Optional[List[str]] = None, __nested_builder__: Optional["Builder"] = None, - **attributes + **attributes, ) -> "Builder": """Create a block within this HCL document.""" @@ -53,7 +53,6 @@ def build(self): ) for block_type, blocks in self.blocks.items(): - for labels, block_builder, nested_blocks in blocks: # build the sub-block block = block_builder.build() @@ -70,9 +69,7 @@ def build(self): return body - def _add_nested_blocks( - self, block: dict, nested_blocks_builder: "Builder" - ) -> "dict": + def _add_nested_blocks(self, block: dict, nested_blocks_builder: "Builder") -> "dict": """Add nested blocks defined within another `Builder` instance to the `block` dictionary""" nested_block = nested_blocks_builder.build() for key, value in nested_block.items(): diff --git a/hcl2/deserializer.py b/hcl2/deserializer.py index 9927f3b0..667e0b20 100644 --- a/hcl2/deserializer.py +++ b/hcl2/deserializer.py @@ -5,63 +5,63 @@ from abc import ABC, abstractmethod from dataclasses import dataclass from functools import cached_property -from typing import Any, TextIO, List, Optional, Union +from typing import Any, List, Optional, TextIO, Union from regex import regex +from hcl2.const import COMMENTS_KEY, INLINE_COMMENTS_KEY, IS_BLOCK from hcl2.parser import parser as _get_parser -from hcl2.const import IS_BLOCK, COMMENTS_KEY, INLINE_COMMENTS_KEY from hcl2.rules.abstract import LarkElement, LarkRule from hcl2.rules.base import ( - BlockRule, AttributeRule, + BlockRule, BodyRule, StartRule, ) from hcl2.rules.containers import ( - TupleRule, - ObjectRule, - ObjectElemRule, ObjectElemKeyExpressionRule, ObjectElemKeyRule, + ObjectElemRule, + ObjectRule, + TupleRule, ) from hcl2.rules.expressions import ExprTermRule from hcl2.rules.literal_rules import ( + FloatLitRule, IdentifierRule, IntLitRule, - FloatLitRule, LiteralValueRule, ) from hcl2.rules.strings import ( - StringRule, - InterpolationRule, - StringPartRule, HeredocTemplateRule, HeredocTrimTemplateRule, + InterpolationRule, + StringPartRule, + StringRule, ) from hcl2.rules.tokens import ( - NAME, - EQ, + COLON, + COMMA, DBLQUOTE, - STRING_CHARS, + EQ, ESCAPED_INTERPOLATION, + FALSE, + HEREDOC_TEMPLATE, + HEREDOC_TRIM_TEMPLATE, INTERP_START, + LBRACE, + LSQB, + NAME, + NULL, RBRACE, - IntLiteral, - FloatLiteral, RSQB, - LSQB, - COMMA, - LBRACE, - HEREDOC_TRIM_TEMPLATE, - HEREDOC_TEMPLATE, - COLON, + STRING_CHARS, TRUE, - FALSE, - NULL, + FloatLiteral, + IntLiteral, ) from hcl2.transformer import RuleTransformer -from hcl2.utils import HEREDOC_TRIM_PATTERN, HEREDOC_PATTERN +from hcl2.utils import HEREDOC_PATTERN, HEREDOC_TRIM_PATTERN @dataclass @@ -107,9 +107,7 @@ def _transformer(self) -> RuleTransformer: def load_python(self, value: Any) -> StartRule: """Deserialize a Python object into a StartRule tree.""" if not isinstance(value, dict): - raise TypeError( - f"Expected dict for top-level HCL body, got {type(value).__name__}" - ) + raise TypeError(f"Expected dict for top-level HCL body, got {type(value).__name__}") # Top-level dict is always a body (attributes + blocks), not an object children = self._deserialize_block_elements(value) return StartRule([BodyRule(children)]) @@ -121,7 +119,6 @@ def loads(self, value: str) -> LarkElement: def _deserialize(self, value: Any) -> LarkElement: if isinstance(value, dict): if self._contains_block_marker(value): - children: List[Any] = [] block_elements = self._deserialize_block_elements(value) @@ -250,11 +247,7 @@ def _deserialize_string_part(self, value: str) -> StringPartRule: if value.startswith("${") and value.endswith("}"): return StringPartRule( - [ - InterpolationRule( - [INTERP_START(), self._deserialize_expression(value), RBRACE()] - ) - ] + [InterpolationRule([INTERP_START(), self._deserialize_expression(value), RBRACE()])] ) return StringPartRule([STRING_CHARS(value)]) @@ -402,8 +395,6 @@ def _contains_block_marker(self, obj: dict) -> bool: return True if isinstance(value, list): for element in value: - if isinstance(element, dict) and self._contains_block_marker( - element - ): + if isinstance(element, dict) and self._contains_block_marker(element): return True return False diff --git a/hcl2/formatter.py b/hcl2/formatter.py index f45a37ff..e30cc1a7 100644 --- a/hcl2/formatter.py +++ b/hcl2/formatter.py @@ -1,30 +1,31 @@ """Format LarkElement trees with indentation, alignment, and spacing.""" + from abc import ABC, abstractmethod from dataclasses import dataclass from typing import List, Optional from hcl2.rules.abstract import LarkElement, LarkRule from hcl2.rules.base import ( - StartRule, - BlockRule, AttributeRule, + BlockRule, BodyRule, + StartRule, ) from hcl2.rules.containers import ( - ObjectRule, - ObjectElemRule, ObjectElemKeyExpressionRule, + ObjectElemRule, + ObjectRule, TupleRule, ) from hcl2.rules.expressions import ExprTermRule -from hcl2.rules.functions import FunctionCallRule from hcl2.rules.for_expressions import ( - ForTupleExprRule, - ForObjectExprRule, - ForIntroRule, ForCondRule, + ForIntroRule, + ForObjectExprRule, + ForTupleExprRule, ) -from hcl2.rules.tokens import NL_OR_COMMENT, LBRACE, COLON, LSQB, COMMA +from hcl2.rules.functions import FunctionCallRule +from hcl2.rules.tokens import COLON, COMMA, LBRACE, LSQB, NL_OR_COMMENT from hcl2.rules.whitespace import NewLineOrCommentRule @@ -222,9 +223,7 @@ def format_fortupleexpr(self, expression: ForTupleExprRule, indent_level: int = expression.children[7] = self._build_newline(indent_level) self._deindent_last_line() - def format_forobjectexpr( - self, expression: ForObjectExprRule, indent_level: int = 0 - ): + def format_forobjectexpr(self, expression: ForObjectExprRule, indent_level: int = 0): """Format a for-object expression with newlines around clauses.""" for child in expression.children: if isinstance(child, ExprTermRule): @@ -274,9 +273,7 @@ def _vertically_align_attributes_in_body(self, body: BodyRule): self._align_attributes_sequence(attributes_sequence) def _align_attributes_sequence(self, attributes_sequence: List[AttributeRule]): - max_length = max( - len(attribute.identifier.token.value) for attribute in attributes_sequence - ) + max_length = max(len(attribute.identifier.token.value) for attribute in attributes_sequence) for attribute in attributes_sequence: name_length = len(attribute.identifier.token.value) spaces_to_add = max_length - name_length @@ -310,15 +307,9 @@ def _key_text_width(key: LarkElement) -> int: width -= 3 return width - def _build_newline( - self, next_line_indent: int = 0, count: int = 1 - ) -> NewLineOrCommentRule: + def _build_newline(self, next_line_indent: int = 0, count: int = 1) -> NewLineOrCommentRule: result = NewLineOrCommentRule( - [ - NL_OR_COMMENT( - ("\n" * count) + " " * self.options.indent_length * next_line_indent - ) - ] + [NL_OR_COMMENT(("\n" * count) + " " * self.options.indent_length * next_line_indent)] ) self._last_new_line = result return result diff --git a/hcl2/parser.py b/hcl2/parser.py index d275a589..0ba6d726 100644 --- a/hcl2/parser.py +++ b/hcl2/parser.py @@ -1,4 +1,5 @@ """A parser for HCL2 implemented using the Lark parser""" + import functools from pathlib import Path @@ -6,7 +7,6 @@ from hcl2.postlexer import PostLexer - PARSER_FILE = Path(__file__).absolute().resolve().parent / ".lark_cache.bin" diff --git a/hcl2/query/__init__.py b/hcl2/query/__init__.py index 39ce9e5d..f423c546 100644 --- a/hcl2/query/__init__.py +++ b/hcl2/query/__init__.py @@ -1,23 +1,23 @@ """Query facades for navigating HCL2 LarkElement trees.""" -from hcl2.query._base import NodeView, view_for, register_view -from hcl2.query.body import DocumentView, BodyView -from hcl2.query.blocks import BlockView +from hcl2.query._base import NodeView, register_view, view_for from hcl2.query.attributes import AttributeView -from hcl2.query.containers import TupleView, ObjectView -from hcl2.query.for_exprs import ForTupleView, ForObjectView -from hcl2.query.functions import FunctionCallView +from hcl2.query.blocks import BlockView +from hcl2.query.body import BodyView, DocumentView +from hcl2.query.builtins import BUILTIN_NAMES, apply_builtin +from hcl2.query.containers import ObjectView, TupleView from hcl2.query.expressions import ConditionalView +from hcl2.query.for_exprs import ForObjectView, ForTupleView +from hcl2.query.functions import FunctionCallView from hcl2.query.pipeline import ( - split_pipeline, - classify_stage, - execute_pipeline, - PathStage, BuiltinStage, + PathStage, SelectStage, + classify_stage, + execute_pipeline, + split_pipeline, ) -from hcl2.query.builtins import apply_builtin, BUILTIN_NAMES -from hcl2.query.predicate import parse_predicate, evaluate_predicate +from hcl2.query.predicate import evaluate_predicate, parse_predicate __all__ = [ "NodeView", diff --git a/hcl2/query/_base.py b/hcl2/query/_base.py index 27421410..b1a22a91 100644 --- a/hcl2/query/_base.py +++ b/hcl2/query/_base.py @@ -10,9 +10,9 @@ TypeVar, ) +from hcl2 import walk as _walk_mod from hcl2.rules.abstract import LarkElement, LarkRule from hcl2.utils import SerializationOptions -from hcl2 import walk as _walk_mod T = TypeVar("T", bound=LarkRule) diff --git a/hcl2/query/blocks.py b/hcl2/query/blocks.py index 2a5fd6cf..269f2209 100644 --- a/hcl2/query/blocks.py +++ b/hcl2/query/blocks.py @@ -76,9 +76,7 @@ def to_dict(self, options: Optional[SerializationOptions] = None) -> Any: result[COMMENTS_KEY] = self._adjacent_comments + existing return result - def blocks( - self, block_type: Optional[str] = None, *labels: str - ) -> List["NodeView"]: + def blocks(self, block_type: Optional[str] = None, *labels: str) -> List["NodeView"]: """Delegate to body.""" from hcl2.query.body import BodyView diff --git a/hcl2/query/body.py b/hcl2/query/body.py index a3bce89e..b9f2ce54 100644 --- a/hcl2/query/body.py +++ b/hcl2/query/body.py @@ -59,9 +59,7 @@ def body(self) -> "BodyView": node: StartRule = self._node # type: ignore[assignment] return BodyView(node.body) - def blocks( - self, block_type: Optional[str] = None, *labels: str - ) -> List["NodeView"]: + def blocks(self, block_type: Optional[str] = None, *labels: str) -> List["NodeView"]: """Return matching blocks, delegating to body.""" return self.body.blocks(block_type, *labels) @@ -78,9 +76,7 @@ def attribute(self, name: str) -> Optional["NodeView"]: class BodyView(NodeView): """View over an HCL2 body (BodyRule).""" - def blocks( - self, block_type: Optional[str] = None, *labels: str - ) -> List["NodeView"]: + def blocks(self, block_type: Optional[str] = None, *labels: str) -> List["NodeView"]: """Return blocks, optionally filtered by type and labels.""" from hcl2.query.blocks import BlockView @@ -97,7 +93,7 @@ def blocks( name_lbls = block_view.name_labels if len(labels) > len(name_lbls): continue - if any(l != nl for l, nl in zip(labels, name_lbls)): + if any(label != nl for label, nl in zip(labels, name_lbls)): continue results.append(block_view) return results diff --git a/hcl2/query/diff.py b/hcl2/query/diff.py index 432af783..31a4775b 100644 --- a/hcl2/query/diff.py +++ b/hcl2/query/diff.py @@ -24,13 +24,9 @@ def diff_dicts(left: Any, right: Any, path: str = "") -> List[DiffEntry]: for key in all_keys: child_path = f"{path}.{key}" if path else key if key not in left: - entries.append( - DiffEntry(path=child_path, kind="added", right=right[key]) - ) + entries.append(DiffEntry(path=child_path, kind="added", right=right[key])) elif key not in right: - entries.append( - DiffEntry(path=child_path, kind="removed", left=left[key]) - ) + entries.append(DiffEntry(path=child_path, kind="removed", left=left[key])) else: entries.extend(diff_dicts(left[key], right[key], child_path)) elif isinstance(left, list) and isinstance(right, list): @@ -44,9 +40,7 @@ def diff_dicts(left: Any, right: Any, path: str = "") -> List[DiffEntry]: else: entries.extend(diff_dicts(left[i], right[i], child_path)) elif left != right: - entries.append( - DiffEntry(path=path or "(root)", kind="changed", left=left, right=right) - ) + entries.append(DiffEntry(path=path or "(root)", kind="changed", left=left, right=right)) return entries @@ -62,9 +56,7 @@ def format_diff_text(entries: List[DiffEntry]) -> str: elif entry.kind == "removed": lines.append(f"- {entry.path}: {_fmt_val(entry.left)}") elif entry.kind == "changed": - lines.append( - f"~ {entry.path}: {_fmt_val(entry.left)} -> {_fmt_val(entry.right)}" - ) + lines.append(f"~ {entry.path}: {_fmt_val(entry.left)} -> {_fmt_val(entry.right)}") return "\n".join(lines) diff --git a/hcl2/query/introspect.py b/hcl2/query/introspect.py index 55c06234..fed8932b 100644 --- a/hcl2/query/introspect.py +++ b/hcl2/query/introspect.py @@ -3,7 +3,7 @@ import inspect from typing import Any, Dict, List -from hcl2.query._base import NodeView, _VIEW_REGISTRY +from hcl2.query._base import _VIEW_REGISTRY, NodeView from hcl2.query.safe_eval import _SAFE_CALLABLE_NAMES @@ -56,8 +56,8 @@ def _describe_view(view: NodeView) -> Dict[str, Any]: def _summarize_view(view: NodeView) -> str: """Generate a brief summary string for a view.""" - from hcl2.query.blocks import BlockView from hcl2.query.attributes import AttributeView + from hcl2.query.blocks import BlockView if isinstance(view, BlockView): return f"block_type={view.block_type!r}, labels={view.labels!r}" diff --git a/hcl2/query/path.py b/hcl2/query/path.py index de0e4a71..5a89974a 100644 --- a/hcl2/query/path.py +++ b/hcl2/query/path.py @@ -23,9 +23,7 @@ class PathSegment: # Optional type qualifier prefix: type_filter:name~?[bracket]? -_SEGMENT_RE = re.compile( - r"^(?:([a-z_]+):)?([a-zA-Z_][a-zA-Z0-9_-]*|\*)(~)?(?:\[(\*|[0-9]+)\])?\??$" -) +_SEGMENT_RE = re.compile(r"^(?:([a-z_]+):)?([a-zA-Z_][a-zA-Z0-9_-]*|\*)(~)?(?:\[(\*|[0-9]+)\])?\??$") def parse_path(path_str: str) -> List[PathSegment]: # pylint: disable=too-many-locals @@ -209,9 +207,7 @@ def _extract_select(part: str) -> Optional[tuple]: # pylint: disable=too-many-l return None seg_name = part[:idx] - if not seg_name or not re.match( - r"^(?:[a-z_]+:)?(?:[a-zA-Z_][a-zA-Z0-9_-]*|\*)~?$", seg_name - ): + if not seg_name or not re.match(r"^(?:[a-z_]+:)?(?:[a-zA-Z_][a-zA-Z0-9_-]*|\*)~?$", seg_name): raise QuerySyntaxError(f"Invalid segment name before [select(): {seg_name!r}") # Parse optional type_filter:name prefix @@ -245,9 +241,7 @@ def _extract_select(part: str) -> Optional[tuple]: # pylint: disable=too-many-l if clean_tail: tail_match = re.match(r"^\[(\*|[0-9]+)\]$", clean_tail) if not tail_match: - raise QuerySyntaxError( - f"Unexpected suffix after [select(...)]: {tail!r} in {part!r}" - ) + raise QuerySyntaxError(f"Unexpected suffix after [select(...)]: {tail!r} in {part!r}") bracket = tail_match.group(1) if bracket == "*": select_all = True diff --git a/hcl2/query/pipeline.py b/hcl2/query/pipeline.py index 06b163fe..187e10f1 100644 --- a/hcl2/query/pipeline.py +++ b/hcl2/query/pipeline.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from typing import Any, List, Tuple -from hcl2.query.path import QuerySyntaxError, PathSegment, parse_path +from hcl2.query.path import PathSegment, QuerySyntaxError, parse_path @dataclass(frozen=True) @@ -89,9 +89,7 @@ def split_pipeline(query_str: str) -> List[str]: elif char == "}": brace_depth -= 1 current.append(char) - elif ( - char == "|" and bracket_depth == 0 and paren_depth == 0 and brace_depth == 0 - ): + elif char == "|" and bracket_depth == 0 and paren_depth == 0 and brace_depth == 0: stage = "".join(current).strip() if not stage: raise QuerySyntaxError("Empty stage in pipeline") @@ -311,9 +309,7 @@ def execute_pipeline(root: Any, stages: List[Any], file_path: str = "") -> List[ # see underlying values instead of wrapper views. # Don't unwrap for ConstructStage — it needs original views # for property access like .block_type, .name_labels. - if i < len(stages) - 1 and not isinstance( - stages[i + 1], (PathStage, ConstructStage) - ): + if i < len(stages) - 1 and not isinstance(stages[i + 1], (PathStage, ConstructStage)): next_results = _unwrap_for_next_stage(next_results) elif isinstance(stage, BuiltinStage): diff --git a/hcl2/query/predicate.py b/hcl2/query/predicate.py index cf8756c0..fa0e3850 100644 --- a/hcl2/query/predicate.py +++ b/hcl2/query/predicate.py @@ -24,7 +24,6 @@ from hcl2.query.path import QuerySyntaxError - # --------------------------------------------------------------------------- # AST nodes # --------------------------------------------------------------------------- @@ -137,9 +136,7 @@ def tokenize(text: str) -> List[Token]: while pos < len(text): match = _TOKEN_RE.match(text, pos) if match is None: - raise QuerySyntaxError( - f"Unexpected character at position {pos} in predicate: {text!r}" - ) + raise QuerySyntaxError(f"Unexpected character at position {pos} in predicate: {text!r}") pos = match.end() kind = match.lastgroup assert kind is not None @@ -293,23 +290,16 @@ def _accessor(self) -> Accessor: builtin = word_tok.value self._expect("LPAREN") arg_tok = self._expect("STRING") - builtin_arg = ( - arg_tok.value[1:-1].replace('\\"', '"').replace("\\\\", "\\") - ) + builtin_arg = arg_tok.value[1:-1].replace('\\"', '"').replace("\\\\", "\\") self._expect("RPAREN") elif word_tok.value == "not": builtin = "not" elif word_tok.value in BUILTIN_NAMES: builtin = word_tok.value else: - raise QuerySyntaxError( - f"Expected builtin or string function after |, " - f"got {word_tok.value!r}" - ) + raise QuerySyntaxError(f"Expected builtin or string function after |, got {word_tok.value!r}") - return Accessor( - parts=parts, index=index, builtin=builtin, builtin_arg=builtin_arg - ) + return Accessor(parts=parts, index=index, builtin=builtin, builtin_arg=builtin_arg) def _literal(self) -> Any: # pylint: disable=too-many-return-statements """Parse a literal value (string, number, boolean, or null).""" @@ -430,9 +420,7 @@ def _resolve_accessor( # pylint: disable=too-many-return-statements if accessor.builtin == "not": return not (current is not None and current is not False and current != 0) if accessor.builtin_arg is not None: - return _apply_string_function( - accessor.builtin, accessor.builtin_arg, current - ) + return _apply_string_function(accessor.builtin, accessor.builtin_arg, current) if current is not None: current = _apply_accessor_builtin(accessor.builtin, current) diff --git a/hcl2/query/resolver.py b/hcl2/query/resolver.py index 46ec38a0..9e21175a 100644 --- a/hcl2/query/resolver.py +++ b/hcl2/query/resolver.py @@ -100,9 +100,7 @@ def _resolve_segment( # pylint: disable=too-many-return-statements return [] -def _resolve_recursive( - state: _ResolverState, segment: PathSegment -) -> List[_ResolverState]: +def _resolve_recursive(state: _ResolverState, segment: PathSegment) -> List[_ResolverState]: """Recursive descent: try matching segment on the node and all descendants.""" from hcl2.query._base import view_for @@ -265,9 +263,7 @@ def _resolve_on_tuple(node: "NodeView", segment: PathSegment) -> List[_ResolverS return [] -def _resolve_on_function_call( - node: "NodeView", segment: PathSegment -) -> List[_ResolverState]: +def _resolve_on_function_call(node: "NodeView", segment: PathSegment) -> List[_ResolverState]: """Resolve a segment on a FunctionCallView.""" from hcl2.query.functions import FunctionCallView @@ -281,9 +277,7 @@ def _resolve_on_function_call( return [] -def _resolve_on_conditional( - node: "NodeView", segment: PathSegment -) -> List[_ResolverState]: +def _resolve_on_conditional(node: "NodeView", segment: PathSegment) -> List[_ResolverState]: """Resolve a segment on a ConditionalView.""" from hcl2.query.expressions import ConditionalView @@ -299,17 +293,13 @@ def _resolve_on_conditional( return [] -def _apply_index_filter( - candidates: List[_ResolverState], segment: PathSegment -) -> List[_ResolverState]: +def _apply_index_filter(candidates: List[_ResolverState], segment: PathSegment) -> List[_ResolverState]: """Apply type filter, predicate filter, and [*]/[N] index to candidates.""" # Apply type filter if present if segment.type_filter is not None: from hcl2.query._base import view_type_name - candidates = [ - c for c in candidates if view_type_name(c.node) == segment.type_filter - ] + candidates = [c for c in candidates if view_type_name(c.node) == segment.type_filter] # Apply predicate filter if present if segment.predicate is not None: diff --git a/hcl2/query/safe_eval.py b/hcl2/query/safe_eval.py index 12f277d9..f78ae372 100644 --- a/hcl2/query/safe_eval.py +++ b/hcl2/query/safe_eval.py @@ -130,9 +130,7 @@ def _validate(node, depth=0): # Block dunder attribute access (prevents sandbox escapes via # __class__, __subclasses__, __globals__, etc.) if isinstance(node, ast.Attribute) and node.attr.startswith("__"): - raise UnsafeExpressionError( - f"Access to dunder attribute {node.attr!r} is not allowed" - ) + raise UnsafeExpressionError(f"Access to dunder attribute {node.attr!r} is not allowed") # Validate Call nodes if isinstance(node, ast.Call): @@ -145,9 +143,7 @@ def _validate(node, depth=0): if func.id not in _SAFE_CALLABLE_NAMES: raise UnsafeExpressionError(f"Calling {func.id!r} is not allowed") else: - raise UnsafeExpressionError( - "Only method calls and safe built-in calls are allowed" - ) + raise UnsafeExpressionError("Only method calls and safe built-in calls are allowed") for child in ast.iter_child_nodes(node): _validate(child, depth + 1) diff --git a/hcl2/reconstructor.py b/hcl2/reconstructor.py index a6fc4344..166e6c58 100644 --- a/hcl2/reconstructor.py +++ b/hcl2/reconstructor.py @@ -2,27 +2,28 @@ from typing import List, Optional, Union -from lark import Tree, Token +from lark import Token, Tree + from hcl2.rules import tokens from hcl2.rules.base import BlockRule from hcl2.rules.containers import ObjectElemRule from hcl2.rules.directives import ( - TemplateIfRule, - TemplateForRule, - TemplateIfStartRule, TemplateElseRule, + TemplateEndforRule, TemplateEndifRule, + TemplateForRule, TemplateForStartRule, - TemplateEndforRule, + TemplateIfRule, + TemplateIfStartRule, ) -from hcl2.rules.for_expressions import ForIntroRule, ForTupleExprRule, ForObjectExprRule -from hcl2.rules.literal_rules import IdentifierRule, LiteralValueRule -from hcl2.rules.strings import StringRule from hcl2.rules.expressions import ( - ExprTermRule, ConditionalRule, + ExprTermRule, UnaryOpRule, ) +from hcl2.rules.for_expressions import ForIntroRule, ForObjectExprRule, ForTupleExprRule +from hcl2.rules.literal_rules import IdentifierRule, LiteralValueRule +from hcl2.rules.strings import StringRule class HCLReconstructor: @@ -85,30 +86,21 @@ def _should_add_space_before( token_type = current_node.type # Space before '{' in blocks - if ( - token_type == tokens.LBRACE.lark_name() - and parent_rule_name == BlockRule.lark_name() - ): + if token_type == tokens.LBRACE.lark_name() and parent_rule_name == BlockRule.lark_name(): return True # Space around Conditional Expression operators if parent_rule_name == ConditionalRule.lark_name() and ( token_type in [tokens.COLON.lark_name(), tokens.QMARK.lark_name()] - or self._last_token_name - in [tokens.COLON.lark_name(), tokens.QMARK.lark_name()] + or self._last_token_name in [tokens.COLON.lark_name(), tokens.QMARK.lark_name()] ): # COLON may already carry leading whitespace from the grammar - if token_type == tokens.COLON.lark_name() and str( - current_node - ).startswith((" ", "\t")): + if token_type == tokens.COLON.lark_name() and str(current_node).startswith((" ", "\t")): return False return True # Space before colon in for_intro - if ( - parent_rule_name == ForIntroRule.lark_name() - and token_type == tokens.COLON.lark_name() - ): + if parent_rule_name == ForIntroRule.lark_name() and token_type == tokens.COLON.lark_name(): if str(current_node).startswith((" ", "\t")): return False return True @@ -210,10 +202,7 @@ def _should_add_space_before( if parent_rule_name == UnaryOpRule.lark_name(): return False - if ( - token_type in self._binary_op_types - or self._last_token_name in self._binary_op_types - ): + if token_type in self._binary_op_types or self._last_token_name in self._binary_op_types: return True elif isinstance(current_node, Tree): @@ -237,11 +226,10 @@ def _should_add_space_before( return True # Space after QMARK/COLON in conditional expressions - if ( - parent_rule_name == ConditionalRule.lark_name() - and self._last_token_name - in [tokens.COLON.lark_name(), tokens.QMARK.lark_name()] - ): + if parent_rule_name == ConditionalRule.lark_name() and self._last_token_name in [ + tokens.COLON.lark_name(), + tokens.QMARK.lark_name(), + ]: return True # Space after colon in for expressions and object elements @@ -261,9 +249,7 @@ def _should_add_space_before( return False - def _reconstruct_tree( - self, tree: Tree, parent_rule_name: Optional[str] = None - ) -> List[str]: + def _reconstruct_tree(self, tree: Tree, parent_rule_name: Optional[str] = None) -> List[str]: """Recursively reconstruct a Tree node into HCL text fragments.""" result = [] rule_name = tree.data @@ -302,9 +288,7 @@ def _reconstruct_tree( return result - def _reconstruct_token( - self, token: Token, parent_rule_name: Optional[str] = None - ) -> str: + def _reconstruct_token(self, token: Token, parent_rule_name: Optional[str] = None) -> str: """Reconstruct a Token node into HCL text fragments.""" result = str(token.value) if self._should_add_space_before(token, parent_rule_name): diff --git a/hcl2/rules/abstract.py b/hcl2/rules/abstract.py index 554bc44d..c8ba063e 100644 --- a/hcl2/rules/abstract.py +++ b/hcl2/rules/abstract.py @@ -1,12 +1,12 @@ """Abstract base classes for the LarkElement tree intermediate representation.""" from abc import ABC, abstractmethod -from typing import Any, Union, List, Optional, Callable +from typing import Any, Callable, List, Optional, Union from lark import Token, Tree from lark.tree import Meta -from hcl2.utils import SerializationOptions, SerializationContext +from hcl2.utils import SerializationContext, SerializationOptions class LarkElement(ABC): @@ -36,9 +36,7 @@ def to_lark(self) -> Any: raise NotImplementedError() @abstractmethod - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize this element to a Python object (dict, list, str, etc.).""" raise NotImplementedError() @@ -65,9 +63,7 @@ def set_value(self, value: Any): """Set the raw value of this token.""" self._value = value - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize this token using its serialize_conversion callable.""" return self.serialize_conversion(self.value) @@ -93,9 +89,7 @@ class LarkRule(LarkElement, ABC): """ @abstractmethod - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize this rule and its children to a Python object.""" raise NotImplementedError() diff --git a/hcl2/rules/base.py b/hcl2/rules/base.py index dacec8b4..625bd835 100644 --- a/hcl2/rules/base.py +++ b/hcl2/rules/base.py @@ -1,19 +1,18 @@ """Rule classes for HCL2 structural elements (attributes, bodies, blocks).""" from collections import defaultdict -from typing import Tuple, Any, List, Union, Optional +from typing import Any, List, Optional, Tuple, Union from lark.tree import Meta -from hcl2.const import IS_BLOCK, INLINE_COMMENTS_KEY +from hcl2.const import INLINE_COMMENTS_KEY, IS_BLOCK from hcl2.rules.abstract import LarkRule, LarkToken from hcl2.rules.expressions import ExprTermRule from hcl2.rules.literal_rules import IdentifierRule from hcl2.rules.strings import StringRule from hcl2.rules.tokens import EQ, LBRACE, RBRACE - from hcl2.rules.whitespace import NewLineOrCommentRule -from hcl2.utils import SerializationOptions, SerializationContext +from hcl2.utils import SerializationContext, SerializationOptions class AttributeRule(LarkRule): @@ -40,9 +39,7 @@ def expression(self) -> ExprTermRule: """Return the attribute value expression.""" return self._children[2] - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to a single-entry dict.""" return {self.identifier.serialize(options): self.expression.serialize(options)} @@ -63,9 +60,7 @@ def lark_name() -> str: """Return the grammar rule name.""" return "body" - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to a dict, grouping blocks under their type name.""" attribute_names = set() comments = [] @@ -74,7 +69,6 @@ def serialize( result = defaultdict(list) for child in self._children: - if isinstance(child, BlockRule): name = child.labels[0].serialize(options) if name in attribute_names: @@ -117,9 +111,7 @@ def lark_name() -> str: """Return the grammar rule name.""" return "start" - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize by delegating to the body.""" return self.body.serialize(options) @@ -138,9 +130,7 @@ class BlockRule(LarkRule): def __init__(self, children, meta: Optional[Meta] = None): super().__init__(children, meta) - *self._labels, self._body = [ - child for child in children if not isinstance(child, LarkToken) - ] + *self._labels, self._body = [child for child in children if not isinstance(child, LarkToken)] @staticmethod def lark_name() -> str: @@ -157,9 +147,7 @@ def body(self) -> BodyRule: """Return the block body.""" return self._body - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to a nested dict with labels as keys.""" result = self._body.serialize(options) if options.explicit_blocks: diff --git a/hcl2/rules/containers.py b/hcl2/rules/containers.py index 671d98b7..8b811ce8 100644 --- a/hcl2/rules/containers.py +++ b/hcl2/rules/containers.py @@ -1,31 +1,31 @@ """Rule classes for HCL2 tuples, objects, and their elements.""" -from typing import Tuple, List, Optional, Union, Any +from typing import Any, List, Optional, Tuple, Union from hcl2.rules.abstract import LarkRule from hcl2.rules.expressions import ExpressionRule from hcl2.rules.literal_rules import ( FloatLitRule, - IntLitRule, IdentifierRule, + IntLitRule, ) from hcl2.rules.strings import StringRule from hcl2.rules.tokens import ( COLON, + COMMA, EQ, LBRACE, - COMMA, - RBRACE, LSQB, + RBRACE, RSQB, ) from hcl2.rules.whitespace import ( - NewLineOrCommentRule, InlineCommentMixIn, + NewLineOrCommentRule, ) from hcl2.utils import ( - SerializationOptions, SerializationContext, + SerializationOptions, to_dollar_string, ) @@ -58,22 +58,16 @@ def lark_name() -> str: @property def elements(self) -> List[ExpressionRule]: """Return the expression elements of the tuple.""" - return [ - child for child in self.children[1:-1] if isinstance(child, ExpressionRule) - ] + return [child for child in self.children[1:-1] if isinstance(child, ExpressionRule)] - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to a Python list or bracketed string.""" if not options.wrap_tuples and not context.inside_dollar_string: return [element.serialize(options, context) for element in self.elements] with context.modify(inside_dollar_string=True): result = "[" - result += ", ".join( - str(element.serialize(options, context)) for element in self.elements - ) + result += ", ".join(str(element.serialize(options, context)) for element in self.elements) result += "]" if not context.inside_dollar_string: @@ -99,9 +93,7 @@ def value(self) -> key_T: """Return the key value (identifier, string, or number).""" return self._children[0] - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize the key, coercing numbers to strings.""" result = self.value.serialize(options, context) # Object keys must be strings for JSON compatibility @@ -131,9 +123,7 @@ def expression(self) -> ExpressionRule: """Return the key expression.""" return self._children[0] - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to '${expression}' string.""" with context.modify(inside_dollar_string=True): result = str(self.expression.serialize(options, context)) @@ -166,15 +156,9 @@ def expression(self): """Return the value expression.""" return self._children[2] - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to a single-entry dict.""" - return { - self.key.serialize(options, context): self.expression.serialize( - options, context - ) - } + return {self.key.serialize(options, context): self.expression.serialize(options, context)} class ObjectRule(InlineCommentMixIn): @@ -200,13 +184,9 @@ def lark_name() -> str: @property def elements(self) -> List[ObjectElemRule]: """Return the list of object element rules.""" - return [ - child for child in self.children[1:-1] if isinstance(child, ObjectElemRule) - ] + return [child for child in self.children[1:-1] if isinstance(child, ObjectElemRule)] - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to a Python dict or braced string.""" if not options.wrap_objects and not context.inside_dollar_string: dict_result: dict = {} diff --git a/hcl2/rules/directives.py b/hcl2/rules/directives.py index ff9cc532..4a74fc66 100644 --- a/hcl2/rules/directives.py +++ b/hcl2/rules/directives.py @@ -8,19 +8,19 @@ from hcl2.rules.expressions import ExpressionRule from hcl2.rules.literal_rules import IdentifierRule from hcl2.rules.tokens import ( + COMMA, DIRECTIVE_START, - STRIP_MARKER, - IF, ELSE, + ENDFOR, ENDIF, FOR, + IF, IN, - ENDFOR, - COMMA, RBRACE, + STRIP_MARKER, StaticStringToken, ) -from hcl2.utils import SerializationOptions, SerializationContext +from hcl2.utils import SerializationContext, SerializationOptions def _is_strip(child) -> bool: @@ -86,9 +86,7 @@ def strip_close(self) -> bool: """Check if there's a strip marker before }.""" return self._children[4] is not None - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to %{ if EXPR } or %{~ if EXPR ~}.""" with context.modify(inside_dollar_string=True): cond_str = self.condition.serialize(options, context) @@ -127,9 +125,7 @@ def strip_close(self) -> bool: """Check if there's a strip marker before }.""" return self._children[3] is not None - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to %{ else } or %{~ else ~}.""" prefix = _strip_prefix(self.strip_open) suffix = _strip_suffix(self.strip_close) @@ -166,9 +162,7 @@ def strip_close(self) -> bool: """Check if there's a strip marker before }.""" return self._children[3] is not None - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to %{ endif } or %{~ endif ~}.""" prefix = _strip_prefix(self.strip_open) suffix = _strip_suffix(self.strip_close) @@ -253,9 +247,7 @@ def collection(self) -> ExpressionRule: """Return the collection expression after IN.""" return self._children[7] - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to %{ for VAR in EXPR } or %{~ for VAR in EXPR ~}.""" prefix = _strip_prefix(self.strip_open) suffix = _strip_suffix(self.strip_close) @@ -297,9 +289,7 @@ def strip_close(self) -> bool: """Check if there's a strip marker before }.""" return self._children[3] is not None - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to %{ endfor } or %{~ endfor ~}.""" prefix = _strip_prefix(self.strip_open) suffix = _strip_suffix(self.strip_close) @@ -349,9 +339,7 @@ def __init__( # pylint: disable=R0917 children.append(endif) super().__init__(children, meta) - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize the full if/else/endif directive.""" result = self._if_start.serialize(options, context) for part in self._if_body: @@ -407,9 +395,7 @@ def __init__( children = [for_start, *body, endfor] super().__init__(children, meta) - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize the full for/endfor directive.""" result = self._for_start.serialize(options, context) for part in self._body: diff --git a/hcl2/rules/expressions.py b/hcl2/rules/expressions.py index 057e1ffc..5aa5f76e 100644 --- a/hcl2/rules/expressions.py +++ b/hcl2/rules/expressions.py @@ -9,16 +9,16 @@ LarkToken, ) from hcl2.rules.literal_rules import BinaryOperatorRule -from hcl2.rules.tokens import LPAR, RPAR, QMARK, COLON +from hcl2.rules.tokens import COLON, LPAR, QMARK, RPAR from hcl2.rules.whitespace import ( - NewLineOrCommentRule, InlineCommentMixIn, + NewLineOrCommentRule, ) from hcl2.utils import ( - wrap_into_parentheses, - to_dollar_string, - SerializationOptions, SerializationContext, + SerializationOptions, + to_dollar_string, + wrap_into_parentheses, ) @@ -30,9 +30,7 @@ def lark_name() -> str: """?expression is transparent in Lark — subclasses must override.""" raise NotImplementedError("ExpressionRule.lark_name() must be overridden") - def __init__( - self, children, meta: Optional[Meta] = None, parentheses: bool = False - ): + def __init__(self, children, meta: Optional[Meta] = None, parentheses: bool = False): super().__init__(children, meta) self._parentheses = parentheses @@ -100,13 +98,9 @@ def expression(self) -> ExpressionRule: """Return the inner expression.""" return self._children[2] - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize, handling parenthesized expression wrapping.""" - with context.modify( - inside_parentheses=self.parentheses or context.inside_parentheses - ): + with context.modify(inside_parentheses=self.parentheses or context.inside_parentheses): result = self.expression.serialize(options, context) if self.parentheses: @@ -156,9 +150,7 @@ def if_false(self) -> ExpressionRule: """Return the false-branch expression.""" return self._children[8] - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to ternary expression string.""" with context.modify(inside_dollar_string=True): result = ( @@ -205,9 +197,7 @@ def expr_term(self) -> ExprTermRule: """Return the right-hand operand.""" return self._children[3] - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to 'operator operand' string.""" op_str = self.binary_operator.serialize(options, context) term_str = self.expr_term.serialize(options, context) @@ -274,15 +264,11 @@ def absorbed_comments(self): return trailing.to_list() or [] return [] - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to 'lhs operator rhs' string.""" with context.modify(inside_dollar_string=True): lhs = self.expr_term.serialize(options, context) - operator = str( - self.binary_term.binary_operator.serialize(options, context) - ).strip() + operator = str(self.binary_term.binary_operator.serialize(options, context)).strip() rhs = self.binary_term.expr_term.serialize(options, context) result = f"{lhs} {operator} {rhs}" @@ -315,9 +301,7 @@ def expr_term(self): """Return the operand.""" return self._children[1] - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to 'operator operand' string.""" with context.modify(inside_dollar_string=True): operator = self.operator.rstrip() diff --git a/hcl2/rules/for_expressions.py b/hcl2/rules/for_expressions.py index eb018343..6013072e 100644 --- a/hcl2/rules/for_expressions.py +++ b/hcl2/rules/for_expressions.py @@ -1,33 +1,33 @@ """Rule classes for HCL2 for-tuple and for-object expressions.""" from dataclasses import replace -from typing import Any, Tuple, Optional, List +from typing import Any, List, Optional, Tuple from lark.tree import Meta from hcl2.rules.expressions import ExpressionRule from hcl2.rules.literal_rules import IdentifierRule from hcl2.rules.tokens import ( - LSQB, - RSQB, - LBRACE, - RBRACE, - FOR, - IN, - IF, - COMMA, COLON, + COMMA, ELLIPSIS, + FOR, FOR_OBJECT_ARROW, + IF, + IN, + LBRACE, + LSQB, + RBRACE, + RSQB, StaticStringToken, ) from hcl2.rules.whitespace import ( - NewLineOrCommentRule, InlineCommentMixIn, + NewLineOrCommentRule, ) from hcl2.utils import ( - SerializationOptions, SerializationContext, + SerializationOptions, to_dollar_string, ) @@ -92,9 +92,7 @@ def iterable(self) -> ExpressionRule: """Return the collection expression being iterated over.""" return self._children[8] - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> str: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> str: """Serialize to 'for key, value in collection : ' string.""" result = "for " @@ -129,9 +127,7 @@ def condition_expr(self) -> ExpressionRule: """Return the condition expression.""" return self._children[2] - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> str: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> str: """Serialize to 'if condition' string.""" return f"if {self.condition_expr.serialize(options, context)}" @@ -195,9 +191,7 @@ def condition(self) -> Optional[ForCondRule]: """Return the optional condition rule.""" return self._children[6] - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to '[for ... : expr]' string.""" result = "[" @@ -251,11 +245,7 @@ def _insert_optionals( # type: ignore[override] condition = None for child in children: - if ( - ellipsis_ is None - and isinstance(child, StaticStringToken) - and child.lark_name() == "ELLIPSIS" - ): + if ellipsis_ is None and isinstance(child, StaticStringToken) and child.lark_name() == "ELLIPSIS": ellipsis_ = child if condition is None and isinstance(child, ForCondRule): condition = child @@ -297,18 +287,14 @@ def condition(self) -> Optional[ForCondRule]: """Return the optional condition rule.""" return self._children[11] - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to '{for ... : key => value}' string.""" result = "{" with context.modify(inside_dollar_string=True): result += self.for_intro.serialize(options, context) result += f"{self.key_expr.serialize(options, context)} => " - result += self.value_expr.serialize( - replace(options, wrap_objects=True), context - ) + result += self.value_expr.serialize(replace(options, wrap_objects=True), context) if self.ellipsis is not None: result += self.ellipsis.serialize(options, context) diff --git a/hcl2/rules/functions.py b/hcl2/rules/functions.py index bd574ebe..c48a7c2a 100644 --- a/hcl2/rules/functions.py +++ b/hcl2/rules/functions.py @@ -1,17 +1,17 @@ """Rule classes for HCL2 function calls and arguments.""" -from typing import Any, Optional, Tuple, Union, List +from typing import Any, List, Optional, Tuple, Union from hcl2.rules.expressions import ExpressionRule from hcl2.rules.literal_rules import IdentifierRule -from hcl2.rules.tokens import COMMA, ELLIPSIS, StringToken, LPAR, RPAR +from hcl2.rules.tokens import COMMA, ELLIPSIS, LPAR, RPAR, StringToken from hcl2.rules.whitespace import ( InlineCommentMixIn, NewLineOrCommentRule, ) from hcl2.utils import ( - SerializationOptions, SerializationContext, + SerializationOptions, to_dollar_string, ) @@ -50,13 +50,9 @@ def arguments(self) -> List[ExpressionRule]: """Return the list of expression arguments.""" return [child for child in self._children if isinstance(child, ExpressionRule)] - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to a comma-separated argument string.""" - result = ", ".join( - str(argument.serialize(options, context)) for argument in self.arguments - ) + result = ", ".join(str(argument.serialize(options, context)) for argument in self.arguments) if self.has_ellipsis: result += " ..." return result @@ -94,15 +90,10 @@ def arguments(self) -> Optional[ArgumentsRule]: return child return None - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to 'func(args)' string.""" with context.modify(inside_dollar_string=True): - name = "::".join( - identifier.serialize(options, context) - for identifier in self.identifiers - ) + name = "::".join(identifier.serialize(options, context) for identifier in self.identifiers) args = self.arguments args_str = args.serialize(options, context) if args else "" result = f"{name}({args_str})" diff --git a/hcl2/rules/indexing.py b/hcl2/rules/indexing.py index 4cc292c0..9bdab541 100644 --- a/hcl2/rules/indexing.py +++ b/hcl2/rules/indexing.py @@ -1,28 +1,28 @@ """Rule classes for HCL2 indexing, attribute access, and splat expressions.""" -from typing import List, Optional, Tuple, Any, Union +from typing import Any, List, Optional, Tuple, Union from lark.tree import Meta from hcl2.rules.abstract import LarkRule -from hcl2.rules.expressions import ExprTermRule, ExpressionRule +from hcl2.rules.expressions import ExpressionRule, ExprTermRule from hcl2.rules.literal_rules import IdentifierRule from hcl2.rules.tokens import ( + ATTR_SPLAT, DOT, - IntLiteral, + FULL_SPLAT, LSQB, RSQB, - ATTR_SPLAT, - FULL_SPLAT, + IntLiteral, ) from hcl2.rules.whitespace import ( InlineCommentMixIn, NewLineOrCommentRule, ) from hcl2.utils import ( + SerializationContext, SerializationOptions, to_dollar_string, - SerializationContext, ) @@ -44,9 +44,7 @@ def index(self): """Return the index token.""" return self.children[1] - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to '.N' string.""" return f".{self.index.serialize(options, context)}" @@ -72,9 +70,7 @@ def index_expression(self): """Return the index expression inside the brackets.""" return self.children[2] - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to '[expr]' string.""" return f"[{self.index_expression.serialize(options, context)}]" @@ -93,9 +89,7 @@ def lark_name() -> str: """Return the grammar rule name.""" return "index_expr_term" - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to 'expr[index]' string.""" with context.modify(inside_dollar_string=True): expr = self.children[0].serialize(options, context) @@ -124,9 +118,7 @@ def identifier(self) -> IdentifierRule: """Return the accessed identifier.""" return self._children[1] - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to '.identifier' string.""" return f".{self.identifier.serialize(options, context)}" @@ -154,9 +146,7 @@ def get_attr(self) -> GetAttrRule: """Return the attribute access rule.""" return self._children[1] - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to 'expr.attr' string.""" with context.modify(inside_dollar_string=True): expr = self.expr_term.serialize(options, context) @@ -187,13 +177,9 @@ def get_attrs( """Return the trailing accessor chain.""" return self._children[1:] - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to '.*...' string.""" - return ".*" + "".join( - get_attr.serialize(options, context) for get_attr in self.get_attrs - ) + return ".*" + "".join(get_attr.serialize(options, context) for get_attr in self.get_attrs) class AttrSplatExprTermRule(ExpressionRule): @@ -216,9 +202,7 @@ def attr_splat(self) -> AttrSplatRule: """Return the attribute splat rule.""" return self._children[1] - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to 'expr.*...' string.""" with context.modify(inside_dollar_string=True): expr = self.expr_term.serialize(options, context) @@ -250,13 +234,9 @@ def get_attrs( """Return the trailing accessor chain.""" return self._children[1:] - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to '[*]...' string.""" - return "[*]" + "".join( - get_attr.serialize(options, context) for get_attr in self.get_attrs - ) + return "[*]" + "".join(get_attr.serialize(options, context) for get_attr in self.get_attrs) class FullSplatExprTermRule(ExpressionRule): @@ -279,9 +259,7 @@ def attr_splat(self) -> FullSplatRule: """Return the full splat rule.""" return self._children[1] - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to 'expr[*]...' string.""" with context.modify(inside_dollar_string=True): expr = self.expr_term.serialize(options, context) diff --git a/hcl2/rules/literal_rules.py b/hcl2/rules/literal_rules.py index 4422aada..317d149e 100644 --- a/hcl2/rules/literal_rules.py +++ b/hcl2/rules/literal_rules.py @@ -4,7 +4,7 @@ from typing import Any, Tuple from hcl2.rules.abstract import LarkRule, LarkToken -from hcl2.utils import SerializationOptions, SerializationContext, to_dollar_string +from hcl2.utils import SerializationContext, SerializationOptions, to_dollar_string class TokenRule(LarkRule, ABC): @@ -17,9 +17,7 @@ def token(self) -> LarkToken: """Return the single token child.""" return self._children[0] - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize by delegating to the token's own serialization.""" return self.token.serialize() @@ -43,9 +41,7 @@ def lark_name() -> str: """Return the grammar rule name.""" return "literal_value" - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to Python True, False, or None.""" value = self.token.value if context.inside_dollar_string: @@ -79,18 +75,12 @@ def lark_name() -> str: """Return the grammar rule name.""" return "float_lit" - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize, preserving scientific notation when configured.""" value = self.token.value # Scientific notation (e.g. 1.23e5) cannot survive a Python float() # round-trip, so preserve it as a ${...} expression string. - if ( - options.preserve_scientific_notation - and isinstance(value, str) - and "e" in value.lower() - ): + if options.preserve_scientific_notation and isinstance(value, str) and "e" in value.lower(): if context.inside_dollar_string: return value return to_dollar_string(value) diff --git a/hcl2/rules/strings.py b/hcl2/rules/strings.py index f3e456eb..4be161cc 100644 --- a/hcl2/rules/strings.py +++ b/hcl2/rules/strings.py @@ -1,27 +1,27 @@ """Rule classes for HCL2 string literals, interpolation, and heredoc templates.""" import sys -from typing import Tuple, List, Any, Union +from typing import Any, List, Tuple, Union from hcl2.rules.abstract import LarkRule from hcl2.rules.expressions import ExpressionRule from hcl2.rules.tokens import ( - INTERP_START, - RBRACE, DBLQUOTE, - STRING_CHARS, - ESCAPED_INTERPOLATION, ESCAPED_DIRECTIVE, - TEMPLATE_STRING, + ESCAPED_INTERPOLATION, HEREDOC_TEMPLATE, HEREDOC_TRIM_TEMPLATE, + INTERP_START, + RBRACE, + STRING_CHARS, + TEMPLATE_STRING, ) from hcl2.utils import ( - SerializationOptions, + HEREDOC_PATTERN, + HEREDOC_TRIM_PATTERN, SerializationContext, + SerializationOptions, to_dollar_string, - HEREDOC_TRIM_PATTERN, - HEREDOC_PATTERN, ) @@ -44,9 +44,7 @@ def expression(self): """Return the interpolated expression.""" return self.children[1] - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to ${expression} string.""" with context.modify(inside_dollar_string=True): return to_dollar_string(self.expression.serialize(options, context)) @@ -73,9 +71,7 @@ def content(self): """Return the content element (string chars, escape, interpolation, or directive).""" return self._children[0] - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize this string part.""" return self.content.serialize(options, context) @@ -95,9 +91,7 @@ def string_parts(self): """Return the list of string parts between quotes.""" return self.children[1:-1] - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to a quoted string.""" inner = "".join(part.serialize(options, context) for part in self.string_parts) if options.strip_string_quotes: @@ -121,9 +115,7 @@ def heredoc(self): """Return the raw heredoc token.""" return self.children[0] - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize the heredoc, optionally stripping to a plain string.""" heredoc = self.heredoc.serialize(options, context) @@ -132,9 +124,7 @@ def serialize( if not match: raise RuntimeError(f"Invalid Heredoc token: {heredoc}") heredoc = match.group(2).rstrip(self._trim_chars) - heredoc = ( - heredoc.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n") - ) + heredoc = heredoc.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n") if options.strip_string_quotes: return heredoc return f'"{heredoc}"' @@ -155,9 +145,7 @@ def lark_name() -> str: """Return the grammar rule name.""" return "heredoc_template_trim" - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize the trim heredoc, stripping common leading whitespace.""" # See https://github.com/hashicorp/hcl2/blob/master/hcl/hclsyntax/spec.md#template-expressions # This is a special version of heredocs that are declared with "<<-" @@ -218,9 +206,7 @@ def inner_value(self) -> str: return raw[2:-2] return raw - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize preserving escaped-quote delimiters for round-trip fidelity. Inside template directive expressions, strings are delimited by \\" diff --git a/hcl2/rules/tokens.py b/hcl2/rules/tokens.py index 807791a6..6fa0f215 100644 --- a/hcl2/rules/tokens.py +++ b/hcl2/rules/tokens.py @@ -1,7 +1,7 @@ """Token classes for terminal elements in the LarkElement tree.""" from functools import lru_cache -from typing import Callable, Any, Dict, Type, Optional, Tuple +from typing import Any, Callable, Dict, Optional, Tuple, Type from hcl2.rules.abstract import LarkToken @@ -15,9 +15,7 @@ class StringToken(LarkToken): @staticmethod def lark_name() -> str: """Overridden by dynamic subclasses created via ``__class_getitem__``.""" - raise NotImplementedError( - "Use StringToken['NAME'] to create a concrete subclass" - ) + raise NotImplementedError("Use StringToken['NAME'] to create a concrete subclass") @classmethod @lru_cache(maxsize=None) @@ -51,9 +49,7 @@ class StaticStringToken(StringToken): @classmethod @lru_cache(maxsize=None) - def __build_subclass( - cls, name: str, default_value: Optional[str] = None - ) -> Type["StringToken"]: + def __build_subclass(cls, name: str, default_value: Optional[str] = None) -> Type["StringToken"]: """Create a subclass with a constant `lark_name` and default value.""" result = type( # type: ignore diff --git a/hcl2/rules/whitespace.py b/hcl2/rules/whitespace.py index 9cb464f7..cb43590b 100644 --- a/hcl2/rules/whitespace.py +++ b/hcl2/rules/whitespace.py @@ -1,12 +1,12 @@ """Rule classes for whitespace, comments, and inline comment handling.""" from abc import ABC -from typing import Optional, List, Any +from typing import Any, List, Optional from hcl2.rules.abstract import LarkRule from hcl2.rules.literal_rules import TokenRule from hcl2.rules.tokens import NL_OR_COMMENT -from hcl2.utils import SerializationOptions, SerializationContext +from hcl2.utils import SerializationContext, SerializationOptions class NewLineOrCommentRule(TokenRule): @@ -22,9 +22,7 @@ def from_string(cls, string: str) -> "NewLineOrCommentRule": """Create an instance from a raw comment or newline string.""" return cls([NL_OR_COMMENT(string)]) # type: ignore[abstract] # pylint: disable=abstract-class-instantiated - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ) -> Any: + def serialize(self, options=SerializationOptions(), context=SerializationContext()) -> Any: """Serialize to the raw comment/newline string.""" return "".join(child.serialize() for child in self._children) @@ -38,9 +36,7 @@ def is_inline(self) -> bool: """ return not self.serialize().startswith("\n") - def to_list( - self, options: SerializationOptions = SerializationOptions() - ) -> Optional[List[dict]]: + def to_list(self, options: SerializationOptions = SerializationOptions()) -> Optional[List[dict]]: """Extract comment objects, or None if only a newline.""" raw = self.serialize(options) if raw == "\n": @@ -91,7 +87,6 @@ def inline_comments(self): """Collect all inline comment strings from this rule's children.""" result = [] for child in self._children: - if isinstance(child, NewLineOrCommentRule): comments = child.to_list() if comments is not None: diff --git a/hcl2/transformer.py b/hcl2/transformer.py index a7057c70..2d5e9a64 100644 --- a/hcl2/transformer.py +++ b/hcl2/transformer.py @@ -1,78 +1,78 @@ """Transform Lark parse trees into typed LarkElement rule trees.""" # pylint: disable=missing-function-docstring,unused-argument -from lark import Token, Tree, v_args, Transformer, Discard +from lark import Discard, Token, Transformer, Tree, v_args from lark.tree import Meta from hcl2.rules.base import ( - StartRule, - BodyRule, - BlockRule, AttributeRule, + BlockRule, + BodyRule, + StartRule, ) from hcl2.rules.containers import ( - ObjectRule, - ObjectElemRule, + ObjectElemKeyExpressionRule, ObjectElemKeyRule, + ObjectElemRule, + ObjectRule, TupleRule, - ObjectElemKeyExpressionRule, +) +from hcl2.rules.directives import ( + TemplateElseRule, + TemplateEndforRule, + TemplateEndifRule, + TemplateForRule, + TemplateForStartRule, + TemplateIfRule, + TemplateIfStartRule, ) from hcl2.rules.expressions import ( - BinaryTermRule, - UnaryOpRule, BinaryOpRule, - ExprTermRule, + BinaryTermRule, ConditionalRule, + ExprTermRule, + UnaryOpRule, ) from hcl2.rules.for_expressions import ( - ForTupleExprRule, - ForObjectExprRule, - ForIntroRule, ForCondRule, + ForIntroRule, + ForObjectExprRule, + ForTupleExprRule, ) from hcl2.rules.functions import ArgumentsRule, FunctionCallRule from hcl2.rules.indexing import ( - IndexExprTermRule, - SqbIndexRule, - ShortIndexRule, - GetAttrRule, - GetAttrExprTermRule, AttrSplatExprTermRule, AttrSplatRule, - FullSplatRule, FullSplatExprTermRule, + FullSplatRule, + GetAttrExprTermRule, + GetAttrRule, + IndexExprTermRule, + ShortIndexRule, + SqbIndexRule, ) from hcl2.rules.literal_rules import ( + BinaryOperatorRule, FloatLitRule, - IntLitRule, IdentifierRule, - BinaryOperatorRule, + IntLitRule, KeywordRule, LiteralValueRule, ) from hcl2.rules.strings import ( - InterpolationRule, - StringRule, - StringPartRule, HeredocTemplateRule, HeredocTrimTemplateRule, + InterpolationRule, + StringPartRule, + StringRule, TemplateStringRule, ) -from hcl2.rules.directives import ( - TemplateIfRule, - TemplateForRule, - TemplateIfStartRule, - TemplateElseRule, - TemplateEndifRule, - TemplateForStartRule, - TemplateEndforRule, -) from hcl2.rules.tokens import ( NAME, - IntLiteral, FloatLiteral, - StringToken, + IntLiteral, StaticStringToken, + StringToken, ) from hcl2.rules.whitespace import NewLineOrCommentRule @@ -141,9 +141,7 @@ def attribute(self, meta: Meta, args) -> AttributeRule: return AttributeRule(args, meta) @v_args(meta=True) - def new_line_or_comment( - self, meta: Meta, args - ): # -> NewLineOrCommentRule | Discard + def new_line_or_comment(self, meta: Meta, args): # -> NewLineOrCommentRule | Discard if self.discard_new_line_or_comments: return Discard return NewLineOrCommentRule(args, meta) @@ -243,9 +241,7 @@ def _assemble_template_if(self, parts, start_idx, meta: Meta): def _assemble_template_for(self, parts, start_idx, meta: Meta): """Assemble a TemplateForRule from flat parts starting at start_idx.""" for_start = parts[start_idx].content - body, end, i = self._collect_body( - parts, start_idx + 1, (TemplateEndforRule,), meta - ) + body, end, i = self._collect_body(parts, start_idx + 1, (TemplateEndforRule,), meta) if not isinstance(end, TemplateEndforRule): raise RuntimeError("Unterminated template for directive") return TemplateForRule(for_start, body, end, meta), i diff --git a/hcl2/utils.py b/hcl2/utils.py index d701ae25..47201033 100644 --- a/hcl2/utils.py +++ b/hcl2/utils.py @@ -1,4 +1,5 @@ """Serialization options, context tracking, and string utility helpers.""" + import re from contextlib import contextmanager from dataclasses import dataclass, replace diff --git a/hcl2/walk.py b/hcl2/walk.py index be2e4bc0..97d9f6b5 100644 --- a/hcl2/walk.py +++ b/hcl2/walk.py @@ -45,9 +45,7 @@ def find_first(node: LarkElement, rule_type: Type[T]) -> Optional[T]: return None -def find_by_predicate( - node: LarkElement, predicate: Callable[[LarkElement], bool] -) -> Iterator[LarkElement]: +def find_by_predicate(node: LarkElement, predicate: Callable[[LarkElement], bool]) -> Iterator[LarkElement]: """Find all descendants matching an arbitrary predicate.""" for element in walk(node): if predicate(element): diff --git a/pylintrc b/pylintrc deleted file mode 100644 index 05707ffb..00000000 --- a/pylintrc +++ /dev/null @@ -1,230 +0,0 @@ -[MASTER] - -# Specify a configuration file. -#rcfile= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Add to the black list. It should be a base name, not a -# path. You may set this option multiple times. -ignore=CVS,version.py - -# Pickle collected data for later comparisons. -persistent=yes - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - - -[MESSAGES CONTROL] - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time. -#enable= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). -# F0401: *Unable to import %r* -# E0611: *No name %r in module %r* -# E1101: *%s %r has no %r member* -# W0212: *Access to a protected member %s of a client class* -# w0703: Allow catching Exception -# R0801: 1: Similar lines in 2 files, badamson: had trouble disabling this locally -# FIXME: should be re-enabled after it's fixed -# hbrown: I don't think R0801 can be disabled locally -# http://www.logilab.org/ticket/6905 -# pylint #6905: R0801 message cannot be disabled locally [open] -# -# Amplify/Disco customizations: -# W0511: TODO - we want to have TODOs during prototyping -# E1103: %s %r has no %r member (but some types could not be inferred) - fails to infer real members of types, e.g. in Celery -# W0231: method from base class is not called - complains about not invoking empty __init__s in parents, which is annoying -# R0921: abstract class not referenced, when in fact referenced from another egg -# C0415: import-outside-toplevel - needed for circular dep avoidance in query package -# W1113: keyword-arg-before-vararg - intentional API design (blocks(block_type=None, *labels)) -# R0912: too-many-branches - introspect schema builder needs the branches -disable=F0401,E0611,E1101,W0212,W0703,R0801,R0901,W0511,E1103,W0231,C0415,W1113,R0912,R0401 - - -[REPORTS] - -# Set the output format. Available formats are text, parseable, colorized, msvs -# (visual studio) and html -output-format=colorized - -# Tells whether to display a full report or only the messages -reports=no - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (R0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - - -[BASIC] - -# Regular expression which should only match correct module names -module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Regular expression which should only match correct module level names -const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__)|([a-z_][a-z0-9_]*)) - -# Regular expression which should only match correct class names -class-rgx=[A-Z_][a-zA-Z0-9]+$ - -# Regular expression which should only match correct function names -# Amplify: Up to 40 characters long -function-rgx=[a-z_][a-z0-9_]{2,40}$ - -# Regular expression which should only match correct method names -# Amplify: Up to 40 characters long -method-rgx=[a-z_][a-z0-9_]{2,40}$ - -# Regular expression which should only match correct instance attribute names -# Amplify: Up to 40 characters long -attr-rgx=[a-z_][a-z0-9_]{2,40}$ - -# Regular expression which should only match correct argument names -# Amplify: Up to 40 characters long -argument-rgx=[a-z_][a-z0-9_]{2,40}$ - -# Regular expression which should only match correct variable names -# Amplify: Up to 40 characters long -variable-rgx=[a-z_][a-z0-9_]{2,40}$ - -# Regular expression which should only match correct list comprehension / -# generator expression variable names -inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ - -# Good variable names which should always be accepted, separated by a comma -good-names=i,j,k,ex,Run,_,setUp,setUpClass,tearDown,f - -# Bad variable names which should always be refused, separated by a comma -bad-names=foo,bar,baz,toto,tutu,tata - -# Regular expression which should only match functions or classes name which do -# not require a docstring -no-docstring-rgx=(__.*__)|(_[a-z0-9_]*) - - -[FORMAT] - -# Maximum number of characters on a single line. -# Amplify: Line length 110 -max-line-length=110 - -# Maximum number of lines in a module -max-module-lines=1000 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=XXX,TODO - - -[SIMILARITIES] - -# Minimum lines number of a similarity. -min-similarity-lines=4 - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - - -[TYPECHECK] - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# List of classes names for which member attributes should not be checked -# (useful for classes with attributes dynamically set). -ignored-classes=SQLObject - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E0201 when accessed. -generated-members=REQUEST,acl_users,aq_parent - - -[VARIABLES] - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# A regular expression matching names used for dummy variables (i.e. not used). -dummy-variables-rgx=_|dummy - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__,__new__,setUp - - -[DESIGN] - -# Maximum number of arguments for function / method -max-args=10 - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore -ignored-argument-names=_.* - -# Maximum number of locals for function / method body -max-locals=15 - -# Maximum number of return / yield for function / method body -max-returns=6 - -# Maximum number of statements in function / method body -max-statements=50 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of attributes for a class (see R0902). -max-attributes=14 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=0 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=100 - - -[IMPORTS] - -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=regsub,string,TERMIOS,Bastion,rexec - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled) -import-graph= - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled) -ext-import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled) -int-import-graph= diff --git a/pyproject.toml b/pyproject.toml index e5591815..8e4a1753 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,3 +50,11 @@ include-package-data = true [tool.setuptools_scm] write_to = "hcl2/version.py" + +[tool.ruff] +line-length = 110 +target-version = "py38" + +[tool.ruff.lint] +# E/W: pycodestyle, F: pyflakes, I: import sorting (replaces isort). +select = ["E", "W", "F", "I"] diff --git a/test-requirements.txt b/test-requirements.txt index 84bad7b6..292428e2 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,7 +1,5 @@ -r requirements.txt # Linting tools -pylint -pycodestyle mypy # Testing tools nose2[coverage_plugin]>=0.12,<1 diff --git a/test/integration/test_cli_subprocess.py b/test/integration/test_cli_subprocess.py index 9de3b4c5..ce4b1226 100644 --- a/test/integration/test_cli_subprocess.py +++ b/test/integration/test_cli_subprocess.py @@ -440,24 +440,18 @@ def test_ndjson_parse_error_structured_json_on_stderr(self): self.assertIn("message", err) def test_ndjson_all_io_fail_with_skip_exits_4(self): - result = _run_hcl2tojson( - "--ndjson", "-s", "/nonexistent/a.tf", "/nonexistent/b.tf" - ) + result = _run_hcl2tojson("--ndjson", "-s", "/nonexistent/a.tf", "/nonexistent/b.tf") # All-fail with IO errors should exit 4 (EXIT_IO_ERROR), not 2 self.assertEqual(result.returncode, 4, f"stderr: {result.stderr}") def test_ndjson_only_filter_skips_empty(self): - result = _run_hcl2tojson( - "--ndjson", "--only", "nonexistent_block_type", str(HCL_DIR / "nulls.tf") - ) + result = _run_hcl2tojson("--ndjson", "--only", "nonexistent_block_type", str(HCL_DIR / "nulls.tf")) self.assertEqual(result.returncode, 0, f"stderr: {result.stderr}") # No NDJSON line emitted when all data is filtered out self.assertEqual(result.stdout.strip(), "") def test_ndjson_json_indent_warning(self): - result = _run_hcl2tojson( - "--ndjson", "--json-indent", "2", str(HCL_DIR / "nulls.tf") - ) + result = _run_hcl2tojson("--ndjson", "--json-indent", "2", str(HCL_DIR / "nulls.tf")) self.assertEqual(result.returncode, 0) self.assertIn("ignored", result.stderr.lower()) @@ -522,9 +516,7 @@ def test_diff_no_differences(self): def test_diff_from_stdin(self): json_text = (JSON_DIR / "nulls.json").read_text() - result = _run_jsontohcl2( - "--diff", str(HCL_RECONSTRUCTED_DIR / "nulls.tf"), "-", stdin=json_text - ) + result = _run_jsontohcl2("--diff", str(HCL_RECONSTRUCTED_DIR / "nulls.tf"), "-", stdin=json_text) self.assertEqual(result.returncode, 0, f"stderr: {result.stderr}") self.assertEqual(result.stdout, "") @@ -586,9 +578,7 @@ def test_semantic_diff_json_output(self): _write_file(hcl_path, "x = 1\n") _write_file(json_path, json.dumps({"x": 2})) - result = _run_jsontohcl2( - "--semantic-diff", hcl_path, "--diff-json", json_path - ) + result = _run_jsontohcl2("--semantic-diff", hcl_path, "--diff-json", json_path) self.assertEqual(result.returncode, 5) entries = json.loads(result.stdout) self.assertEqual(len(entries), 1) @@ -600,9 +590,7 @@ def test_semantic_diff_from_stdin(self): hcl_path = os.path.join(tmpdir, "original.tf") _write_file(hcl_path, "x = 1\n") - result = _run_jsontohcl2( - "--semantic-diff", hcl_path, "-", stdin=json.dumps({"x": 99}) - ) + result = _run_jsontohcl2("--semantic-diff", hcl_path, "-", stdin=json.dumps({"x": 99})) self.assertEqual(result.returncode, 5) self.assertIn("x", result.stdout) @@ -633,9 +621,7 @@ def test_semantic_diff_pipe_composition(self): class TestFragmentMode(TestCase): def test_fragment_from_stdin(self): - result = _run_jsontohcl2( - "--fragment", "-", stdin='{"cpu": 512, "memory": 1024}' - ) + result = _run_jsontohcl2("--fragment", "-", stdin='{"cpu": 512, "memory": 1024}') self.assertEqual(result.returncode, 0, f"stderr: {result.stderr}") self.assertIn("cpu", result.stdout) self.assertIn("512", result.stdout) @@ -669,9 +655,7 @@ def test_fields_does_not_leak_leaf_lists(self): ' tags = { env = "prod" }\n' "}\n" ) - result = _run_hcl2tojson( - "--only", "module", "--fields", "cpu,memory", stdin=hcl - ) + result = _run_hcl2tojson("--only", "module", "--fields", "cpu,memory", stdin=hcl) self.assertEqual(result.returncode, 0, f"stderr: {result.stderr}") data = json.loads(result.stdout) block = data["module"][0]['"test"'] @@ -767,15 +751,11 @@ def test_basename_collision_preserves_directory_structure(self): for root, _dirs, files in os.walk(outdir): for fname in files: out_files.append(os.path.join(root, fname)) - self.assertEqual( - len(out_files), 2, f"Expected 2 output files, got: {out_files}" - ) + self.assertEqual(len(out_files), 2, f"Expected 2 output files, got: {out_files}") # Each should contain different data contents = set() for path in out_files: with open(path, encoding="utf-8") as fobj: contents.add(fobj.read()) - self.assertEqual( - len(contents), 2, "Output files should have different content" - ) + self.assertEqual(len(contents), 2, "Output files should have different content") diff --git a/test/integration/test_round_trip.py b/test/integration/test_round_trip.py index b78141cb..85c56915 100644 --- a/test/integration/test_round_trip.py +++ b/test/integration/test_round_trip.py @@ -55,9 +55,7 @@ def _get_suites() -> List[str]: Override SUITES to run a specific subset, e.g. SUITES = ["config"] """ - return SUITES or sorted( - file.stem for file in HCL2_ORIGINAL_DIR.iterdir() if file.is_file() - ) + return SUITES or sorted(file.stem for file in HCL2_ORIGINAL_DIR.iterdir() if file.is_file()) # set this to arbitrary list of test suites to run, @@ -167,9 +165,7 @@ def test_json_reserialization(self): for suite in _get_suites(): with self.subTest(suite=suite): hcl_path = _get_suite_file(suite, SuiteStep.ORIGINAL) - json_reserialized_path = _get_suite_file( - suite, SuiteStep.JSON_RESERIALIZED - ) + json_reserialized_path = _get_suite_file(suite, SuiteStep.JSON_RESERIALIZED) serialized = _parse_and_serialize(hcl_path.read_text()) actual = _deserialize_and_reserialize(serialized) diff --git a/test/integration/test_specialized.py b/test/integration/test_specialized.py index 60faf194..9048cc18 100644 --- a/test/integration/test_specialized.py +++ b/test/integration/test_specialized.py @@ -12,17 +12,16 @@ from typing import Optional from unittest import TestCase -from test.integration.test_round_trip import ( - _parse_and_serialize, - _deserialize_and_reserialize, - _deserialize_and_reconstruct, - _direct_reconstruct, -) - from hcl2.deserializer import BaseDeserializer, DeserializerOptions from hcl2.formatter import BaseFormatter from hcl2.reconstructor import HCLReconstructor from hcl2.utils import SerializationOptions +from test.integration.test_round_trip import ( + _deserialize_and_reconstruct, + _deserialize_and_reserialize, + _direct_reconstruct, + _parse_and_serialize, +) SPECIAL_DIR = Path(__file__).absolute().parent / "specialized" @@ -128,9 +127,7 @@ def test_json_reserialization(self): hcl_text = self._load_special("template_directives", ".tf") serialized = _parse_and_serialize(hcl_text) actual = _deserialize_and_reserialize(serialized) - expected = json.loads( - self._load_special("template_directives_reserialized", ".json") - ) + expected = json.loads(self._load_special("template_directives_reserialized", ".json")) self.assertEqual(actual, expected) def test_json_to_hcl(self): diff --git a/test/unit/cli/test_hcl_to_json.py b/test/unit/cli/test_hcl_to_json.py index 458fc430..3b41e606 100644 --- a/test/unit/cli/test_hcl_to_json.py +++ b/test/unit/cli/test_hcl_to_json.py @@ -6,9 +6,8 @@ from unittest import TestCase from unittest.mock import patch -from cli.helpers import EXIT_IO_ERROR, EXIT_PARSE_ERROR, EXIT_PARTIAL from cli.hcl_to_json import main - +from cli.helpers import EXIT_IO_ERROR, EXIT_PARSE_ERROR, EXIT_PARTIAL SIMPLE_HCL = "x = 1\n" SIMPLE_JSON_DICT = {"x": 1} @@ -86,9 +85,7 @@ def test_stdin_single_trailing_newline(self): output = stdout.getvalue() self.assertTrue(output.endswith("\n"), "output should end with newline") - self.assertFalse( - output.endswith("\n\n"), "output should not have double trailing newline" - ) + self.assertFalse(output.endswith("\n\n"), "output should not have double trailing newline") def test_directory_mode(self): with tempfile.TemporaryDirectory() as tmpdir: @@ -460,11 +457,7 @@ def test_ndjson_parse_error_is_json(self): self.assertEqual(cm.exception.code, EXIT_PARSE_ERROR) # stderr may contain progress line (filename) before the JSON error - lines = [ - ln - for ln in stderr.getvalue().strip().splitlines() - if ln.startswith("{") - ] + lines = [ln for ln in stderr.getvalue().strip().splitlines() if ln.startswith("{")] self.assertEqual(len(lines), 1) data = json.loads(lines[0]) self.assertEqual(data["error"], "parse_error") @@ -786,9 +779,7 @@ def test_exclude_multiple_types(self): _write_file(path, HCL_WITH_BLOCKS) stdout = StringIO() - with patch( - "sys.argv", ["hcl2tojson", "--exclude", "variable,output", path] - ): + with patch("sys.argv", ["hcl2tojson", "--exclude", "variable,output", path]): with patch("sys.stdout", stdout): main() diff --git a/test/unit/cli/test_helpers.py b/test/unit/cli/test_helpers.py index ad81250a..8ba078f2 100644 --- a/test/unit/cli/test_helpers.py +++ b/test/unit/cli/test_helpers.py @@ -8,9 +8,9 @@ from cli.helpers import ( _collect_files, - _convert_single_file, _convert_directory, _convert_multiple_files, + _convert_single_file, _error, _expand_file_args, ) @@ -630,9 +630,7 @@ def convert(in_f, out_f): out_f.write(in_f.read()) with patch("sys.stderr", stderr), patch("sys.stdout", stdout): - _convert_single_file( - path, None, convert, False, (Exception,), quiet=True - ) + _convert_single_file(path, None, convert, False, (Exception,), quiet=True) self.assertEqual(stderr.getvalue(), "") self.assertIn("hello", stdout.getvalue()) @@ -649,8 +647,6 @@ def convert(in_f, out_f): out_f.write(in_f.read()) with patch("sys.stderr", stderr), patch("sys.stdout", stdout): - _convert_single_file( - path, None, convert, False, (Exception,), quiet=False - ) + _convert_single_file(path, None, convert, False, (Exception,), quiet=False) self.assertIn("test.txt", stderr.getvalue()) diff --git a/test/unit/cli/test_hq.py b/test/unit/cli/test_hq.py index 80ffe87b..3ec9142e 100644 --- a/test/unit/cli/test_hq.py +++ b/test/unit/cli/test_hq.py @@ -239,9 +239,7 @@ def test_raw_integer_unchanged(self): def test_diff_identical_files(self): with tempfile.NamedTemporaryFile( mode="w", suffix=".tf", delete=False - ) as f1, tempfile.NamedTemporaryFile( - mode="w", suffix=".tf", delete=False - ) as f2: + ) as f1, tempfile.NamedTemporaryFile(mode="w", suffix=".tf", delete=False) as f2: f1.write("x = 1\n") f2.write("x = 1\n") f1.flush() @@ -262,9 +260,7 @@ def test_diff_identical_files(self): def test_diff_changed_files(self): with tempfile.NamedTemporaryFile( mode="w", suffix=".tf", delete=False - ) as f1, tempfile.NamedTemporaryFile( - mode="w", suffix=".tf", delete=False - ) as f2: + ) as f1, tempfile.NamedTemporaryFile(mode="w", suffix=".tf", delete=False) as f2: f1.write("x = 1\n") f2.write("x = 2\n") f1.flush() @@ -284,9 +280,7 @@ def test_diff_changed_files(self): def test_diff_json_output(self): with tempfile.NamedTemporaryFile( mode="w", suffix=".tf", delete=False - ) as f1, tempfile.NamedTemporaryFile( - mode="w", suffix=".tf", delete=False - ) as f2: + ) as f1, tempfile.NamedTemporaryFile(mode="w", suffix=".tf", delete=False) as f2: f1.write("x = 1\ny = 2\n") f2.write("x = 1\nz = 3\n") f1.flush() @@ -405,9 +399,7 @@ def test_multi_file_success_masks_parse_error(self): """If any file produces results, exit 0 even if others fail.""" with tempfile.NamedTemporaryFile( mode="w", suffix=".tf", delete=False - ) as good, tempfile.NamedTemporaryFile( - mode="w", suffix=".tf", delete=False - ) as bad: + ) as good, tempfile.NamedTemporaryFile(mode="w", suffix=".tf", delete=False) as bad: good.write("x = 1\n") bad.write("{invalid\n") good.flush() @@ -443,9 +435,7 @@ class TestMultipleFileArgs(TestCase): def test_two_files(self): with tempfile.NamedTemporaryFile( mode="w", suffix=".tf", delete=False - ) as f1, tempfile.NamedTemporaryFile( - mode="w", suffix=".tf", delete=False - ) as f2: + ) as f1, tempfile.NamedTemporaryFile(mode="w", suffix=".tf", delete=False) as f2: f1.write("x = 1\n") f2.write("x = 2\n") f1.flush() @@ -472,9 +462,7 @@ def test_dir_and_file_mix(self): tf_path = os.path.join(tmpdir, "a.tf") with open(tf_path, "w", encoding="utf-8") as f: f.write("x = 1\n") - with tempfile.NamedTemporaryFile( - mode="w", suffix=".tf", delete=False - ) as f2: + with tempfile.NamedTemporaryFile(mode="w", suffix=".tf", delete=False) as f2: f2.write("x = 2\n") f2.flush() try: @@ -515,9 +503,7 @@ def test_glob_pattern_in_file_arg(self): with self.assertRaises(SystemExit) as cm: main() self.assertEqual(cm.exception.code, EXIT_SUCCESS) - lines = [ - line for line in mock_out.getvalue().strip().split("\n") if line - ] + lines = [line for line in mock_out.getvalue().strip().split("\n") if line] # Should have results from both files self.assertEqual(len(lines), 2) @@ -589,11 +575,7 @@ def test_single_file_multi_result(self): with self.assertRaises(SystemExit) as cm: main() self.assertEqual(cm.exception.code, EXIT_SUCCESS) - lines = [ - l - for l in mock_out.getvalue().strip().split("\n") - if l.strip() - ] + lines = [line for line in mock_out.getvalue().strip().split("\n") if line.strip()] self.assertEqual(len(lines), 2) # Each line should be valid JSON for line in lines: @@ -618,9 +600,7 @@ def test_ndjson_with_raw_errors(self): def test_multi_file_ndjson_has_provenance(self): with tempfile.NamedTemporaryFile( mode="w", suffix=".tf", delete=False - ) as f1, tempfile.NamedTemporaryFile( - mode="w", suffix=".tf", delete=False - ) as f2: + ) as f1, tempfile.NamedTemporaryFile(mode="w", suffix=".tf", delete=False) as f2: f1.write("x = 1\n") f2.write("x = 2\n") f1.flush() @@ -634,11 +614,7 @@ def test_multi_file_ndjson_has_provenance(self): with self.assertRaises(SystemExit) as cm: main() self.assertEqual(cm.exception.code, EXIT_SUCCESS) - lines = [ - l - for l in mock_out.getvalue().strip().split("\n") - if l.strip() - ] + lines = [line for line in mock_out.getvalue().strip().split("\n") if line.strip()] self.assertEqual(len(lines), 2) d1 = json.loads(lines[0]) d2 = json.loads(lines[1]) @@ -653,9 +629,7 @@ class TestProvenance(TestCase): def test_multi_file_json_has_file_key(self): with tempfile.NamedTemporaryFile( mode="w", suffix=".tf", delete=False - ) as f1, tempfile.NamedTemporaryFile( - mode="w", suffix=".tf", delete=False - ) as f2: + ) as f1, tempfile.NamedTemporaryFile(mode="w", suffix=".tf", delete=False) as f2: f1.write("x = 1\n") f2.write("x = 2\n") f1.flush() @@ -669,11 +643,7 @@ def test_multi_file_json_has_file_key(self): with self.assertRaises(SystemExit) as cm: main() self.assertEqual(cm.exception.code, EXIT_SUCCESS) - lines = [ - l - for l in mock_out.getvalue().strip().split("\n") - if l.strip() - ] + lines = [line for line in mock_out.getvalue().strip().split("\n") if line.strip()] for line in lines: data = json.loads(line) self.assertIn("__file__", data) @@ -685,9 +655,7 @@ def test_multi_file_json_produces_valid_merged_array(self): """--json with multiple files must produce a single valid JSON array.""" with tempfile.NamedTemporaryFile( mode="w", suffix=".tf", delete=False - ) as f1, tempfile.NamedTemporaryFile( - mode="w", suffix=".tf", delete=False - ) as f2: + ) as f1, tempfile.NamedTemporaryFile(mode="w", suffix=".tf", delete=False) as f2: f1.write("x = 1\n") f2.write("y = 2\n") f1.flush() @@ -763,11 +731,7 @@ def test_with_location_ndjson(self): with self.assertRaises(SystemExit) as cm: main() self.assertEqual(cm.exception.code, EXIT_SUCCESS) - lines = [ - l - for l in mock_out.getvalue().strip().split("\n") - if l.strip() - ] + lines = [line for line in mock_out.getvalue().strip().split("\n") if line.strip()] self.assertTrue(len(lines) >= 2) for line in lines: data = json.loads(line) @@ -1002,9 +966,7 @@ def test_parallel_ndjson(self): with self.assertRaises(SystemExit) as cm: main() self.assertEqual(cm.exception.code, EXIT_SUCCESS) - lines = [ - l for l in mock_out.getvalue().strip().split("\n") if l.strip() - ] + lines = [line for line in mock_out.getvalue().strip().split("\n") if line.strip()] self.assertEqual(len(lines), 25) for line in lines: data = json.loads(line) @@ -1048,7 +1010,5 @@ def test_serial_for_value_mode(self): with self.assertRaises(SystemExit) as cm: main() self.assertEqual(cm.exception.code, EXIT_SUCCESS) - lines = [ - l for l in mock_out.getvalue().strip().split("\n") if l.strip() - ] + lines = [line for line in mock_out.getvalue().strip().split("\n") if line.strip()] self.assertEqual(len(lines), 25) diff --git a/test/unit/cli/test_json_to_hcl.py b/test/unit/cli/test_json_to_hcl.py index f7daaf8f..aebdbc28 100644 --- a/test/unit/cli/test_json_to_hcl.py +++ b/test/unit/cli/test_json_to_hcl.py @@ -9,7 +9,6 @@ from cli.helpers import EXIT_DIFF, EXIT_IO_ERROR, EXIT_PARSE_ERROR, EXIT_PARTIAL from cli.json_to_hcl import main - SIMPLE_JSON_DICT = {"x": 1} SIMPLE_JSON = json.dumps(SIMPLE_JSON_DICT) @@ -648,10 +647,7 @@ def test_value_change_exits_5(self): def test_ignores_formatting_differences(self): """Formatting-only differences (alignment, commas) should not appear.""" hcl = ( - 'resource "aws_instance" "main" {\n' - ' ami = "abc-123"\n' - ' instance_type = "t2.micro"\n' - "}\n" + 'resource "aws_instance" "main" {\n ami = "abc-123"\n instance_type = "t2.micro"\n}\n' ) # JSON has same values (matching hcl2.load() output format) json_data = { @@ -757,9 +753,7 @@ def test_structure_error_exits_2(self): stderr = StringIO() with patch("sys.argv", ["jsontohcl2", path]): - with patch( - "cli.json_to_hcl.dump", side_effect=TypeError("bad structure") - ): + with patch("cli.json_to_hcl.dump", side_effect=TypeError("bad structure")): with patch("sys.stderr", stderr): with self.assertRaises(SystemExit) as cm: main() diff --git a/test/unit/query/test_base.py b/test/unit/query/test_base.py index 2c0a7de0..ef745884 100644 --- a/test/unit/query/test_base.py +++ b/test/unit/query/test_base.py @@ -1,16 +1,15 @@ # pylint: disable=C0103,C0114,C0115,C0116 from unittest import TestCase +# Ensure views are registered +import hcl2.query # noqa: F401,E402 pylint: disable=unused-import from hcl2.query._base import NodeView, view_for from hcl2.rules.base import AttributeRule, BodyRule, StartRule from hcl2.rules.expressions import ExpressionRule, ExprTermRule from hcl2.rules.literal_rules import IdentifierRule -from hcl2.rules.tokens import NAME, EQ +from hcl2.rules.tokens import EQ, NAME from hcl2.utils import SerializationContext, SerializationOptions -# Ensure views are registered -import hcl2.query # noqa: F401,E402 pylint: disable=unused-import - class StubExpression(ExpressionRule): def __init__(self, value): diff --git a/test/unit/query/test_blocks.py b/test/unit/query/test_blocks.py index 80f3a7ac..ae87f3c9 100644 --- a/test/unit/query/test_blocks.py +++ b/test/unit/query/test_blocks.py @@ -74,9 +74,7 @@ class TestBlockViewAdjacentComments(TestCase): _OPTS = SerializationOptions(with_comments=True) def test_adjacent_comments_at_outer_level(self): - doc = DocumentView.parse( - '# about resource\nresource "type" "name" {\n x = 1\n}\n' - ) + doc = DocumentView.parse('# about resource\nresource "type" "name" {\n x = 1\n}\n') block = doc.blocks("resource")[0] result = block.to_dict(options=self._OPTS) # Adjacent comments go at outer level, alongside the label key @@ -84,9 +82,7 @@ def test_adjacent_comments_at_outer_level(self): self.assertNotIn("__comments__", result['"type"']['"name"']) def test_adjacent_separate_from_inner_comments(self): - doc = DocumentView.parse( - '# adjacent\nresource "type" "name" {\n # inner\n x = 1\n}\n' - ) + doc = DocumentView.parse('# adjacent\nresource "type" "name" {\n # inner\n x = 1\n}\n') block = doc.blocks("resource")[0] result = block.to_dict(options=self._OPTS) # Adjacent at outer level diff --git a/test/unit/query/test_body.py b/test/unit/query/test_body.py index 0a8b75bf..18460f33 100644 --- a/test/unit/query/test_body.py +++ b/test/unit/query/test_body.py @@ -1,7 +1,7 @@ # pylint: disable=C0103,C0114,C0115,C0116 from unittest import TestCase -from hcl2.query.body import DocumentView, BodyView, _collect_leading_comments +from hcl2.query.body import BodyView, DocumentView, _collect_leading_comments class TestDocumentView(TestCase): @@ -15,9 +15,7 @@ def test_body(self): self.assertIsInstance(body, BodyView) def test_blocks(self): - doc = DocumentView.parse( - 'resource "aws_instance" "main" {\n ami = "test"\n}\n' - ) + doc = DocumentView.parse('resource "aws_instance" "main" {\n ami = "test"\n}\n') blocks = doc.blocks("resource") self.assertEqual(len(blocks), 1) self.assertEqual(blocks[0].block_type, "resource") @@ -28,9 +26,7 @@ def test_blocks_no_filter(self): self.assertEqual(len(blocks), 2) def test_blocks_with_labels(self): - doc = DocumentView.parse( - 'resource "aws_instance" "main" {}\nresource "aws_s3_bucket" "data" {}\n' - ) + doc = DocumentView.parse('resource "aws_instance" "main" {}\nresource "aws_s3_bucket" "data" {}\n') blocks = doc.blocks("resource", "aws_instance") self.assertEqual(len(blocks), 1) diff --git a/test/unit/query/test_expressions.py b/test/unit/query/test_expressions.py index 5d565497..66827741 100644 --- a/test/unit/query/test_expressions.py +++ b/test/unit/query/test_expressions.py @@ -75,9 +75,7 @@ def test_pipe_to_condition(self): ) doc = self._parse('x = true ? "yes" : "no"\n') - stages = [ - classify_stage(s) for s in split_pipeline("*..conditional:* | .condition") - ] + stages = [classify_stage(s) for s in split_pipeline("*..conditional:* | .condition")] results = execute_pipeline(doc, stages) self.assertEqual(len(results), 1) self.assertEqual(results[0].to_hcl().strip(), "true") diff --git a/test/unit/query/test_for_exprs.py b/test/unit/query/test_for_exprs.py index b165acc9..15e7ab00 100644 --- a/test/unit/query/test_for_exprs.py +++ b/test/unit/query/test_for_exprs.py @@ -2,8 +2,8 @@ from unittest import TestCase from hcl2.query.body import DocumentView -from hcl2.query.for_exprs import ForTupleView, ForObjectView -from hcl2.rules.for_expressions import ForTupleExprRule, ForObjectExprRule +from hcl2.query.for_exprs import ForObjectView, ForTupleView +from hcl2.rules.for_expressions import ForObjectExprRule, ForTupleExprRule from hcl2.walk import find_first diff --git a/test/unit/query/test_pipeline.py b/test/unit/query/test_pipeline.py index d961a763..4b81217a 100644 --- a/test/unit/query/test_pipeline.py +++ b/test/unit/query/test_pipeline.py @@ -118,10 +118,7 @@ def test_single_stage_identity(self): def test_multi_stage_chaining(self): doc = self._make_doc('resource "aws_instance" "main" {\n ami = "test"\n}\n') # Pipe unwraps blocks to body, so chain with body attributes - stages = [ - classify_stage(s) - for s in split_pipeline("resource.aws_instance.main | .ami") - ] + stages = [classify_stage(s) for s in split_pipeline("resource.aws_instance.main | .ami")] results = execute_pipeline(doc, stages) self.assertEqual(len(results), 1) @@ -148,9 +145,7 @@ def test_pipe_builtin(self): def test_pipe_select(self): doc = self._make_doc('variable "a" {\n default = 1\n}\nvariable "b" {}\n') - stages = [ - classify_stage(s) for s in split_pipeline("variable[*] | select(.default)") - ] + stages = [classify_stage(s) for s in split_pipeline("variable[*] | select(.default)")] results = execute_pipeline(doc, stages) self.assertEqual(len(results), 1) diff --git a/test/unit/query/test_predicate.py b/test/unit/query/test_predicate.py index cc0628e8..d8059780 100644 --- a/test/unit/query/test_predicate.py +++ b/test/unit/query/test_predicate.py @@ -387,9 +387,7 @@ def test_parse_all(self): self.assertEqual(pred.accessor.parts, ["items"]) def test_parse_any_with_boolean_combinators(self): - pred = parse_predicate( - 'any(.elements; .type == "function_call" or .type == "tuple")' - ) + pred = parse_predicate('any(.elements; .type == "function_call" or .type == "tuple")') self.assertIsInstance(pred, AnyExpr) self.assertIsInstance(pred.predicate, OrExpr) diff --git a/test/unit/query/test_resolver.py b/test/unit/query/test_resolver.py index 2b70a2d5..59978160 100644 --- a/test/unit/query/test_resolver.py +++ b/test/unit/query/test_resolver.py @@ -19,23 +19,17 @@ def test_block_type(self): self.assertEqual(len(results), 1) def test_block_type_with_label(self): - doc = DocumentView.parse( - 'resource "aws_instance" "main" {\n ami = "test"\n}\n' - ) + doc = DocumentView.parse('resource "aws_instance" "main" {\n ami = "test"\n}\n') results = resolve_path(doc, parse_path("resource.aws_instance")) self.assertEqual(len(results), 1) def test_block_full_path(self): - doc = DocumentView.parse( - 'resource "aws_instance" "main" {\n ami = "test"\n}\n' - ) + doc = DocumentView.parse('resource "aws_instance" "main" {\n ami = "test"\n}\n') results = resolve_path(doc, parse_path("resource.aws_instance.main")) self.assertEqual(len(results), 1) def test_block_attribute(self): - doc = DocumentView.parse( - 'resource "aws_instance" "main" {\n ami = "test"\n}\n' - ) + doc = DocumentView.parse('resource "aws_instance" "main" {\n ami = "test"\n}\n') results = resolve_path(doc, parse_path("resource.aws_instance.main.ami")) self.assertEqual(len(results), 1) @@ -75,9 +69,7 @@ def test_no_label_block(self): self.assertEqual(len(results), 1) def test_wildcard_labels(self): - doc = DocumentView.parse( - 'resource "aws_instance" "main" {}\nresource "aws_s3_bucket" "data" {}\n' - ) + doc = DocumentView.parse('resource "aws_instance" "main" {}\nresource "aws_s3_bucket" "data" {}\n') results = resolve_path(doc, parse_path("resource[*].*")) self.assertEqual(len(results), 2) @@ -154,15 +146,11 @@ def test_resolve_on_unknown_node_type(self): doc = DocumentView.parse("x = 1\n") attr = doc.attribute("x") value_view = attr.value_node - results = resolve_path( - value_view, [PathSegment(name="foo", select_all=False, index=None)] - ) + results = resolve_path(value_view, [PathSegment(name="foo", select_all=False, index=None)]) self.assertEqual(len(results), 0) def test_block_labels_consumed_then_body(self): - doc = DocumentView.parse( - 'resource "aws_instance" "main" {\n ami = "test"\n}\n' - ) + doc = DocumentView.parse('resource "aws_instance" "main" {\n ami = "test"\n}\n') results = resolve_path(doc, parse_path("resource.aws_instance.main.ami")) self.assertEqual(len(results), 1) self.assertEqual(results[0].name, "ami") @@ -177,23 +165,14 @@ def test_recursive_find_nested_attr(self): self.assertEqual(results[0].name, "ami") def test_recursive_deeply_nested(self): - hcl = ( - 'resource "type" "name" {\n' - ' provisioner "local-exec" {\n' - ' command = "echo"\n' - " }\n" - "}\n" - ) + hcl = 'resource "type" "name" {\n provisioner "local-exec" {\n command = "echo"\n }\n}\n' doc = DocumentView.parse(hcl) results = resolve_path(doc, parse_path("resource..command")) self.assertEqual(len(results), 1) self.assertEqual(results[0].name, "command") def test_recursive_multiple_matches(self): - hcl = ( - 'resource "a" "x" {\n ami = "1"\n}\n' - 'resource "b" "y" {\n ami = "2"\n}\n' - ) + hcl = 'resource "a" "x" {\n ami = "1"\n}\nresource "b" "y" {\n ami = "2"\n}\n' doc = DocumentView.parse(hcl) results = resolve_path(doc, parse_path("*..ami")) self.assertEqual(len(results), 2) @@ -215,10 +194,7 @@ def test_recursive_from_root(self): self.assertEqual(len(results), 1) def test_recursive_with_select_all(self): - hcl = ( - 'resource "a" "x" {\n tag = "1"\n}\n' - 'resource "b" "y" {\n tag = "2"\n}\n' - ) + hcl = 'resource "a" "x" {\n tag = "1"\n}\nresource "b" "y" {\n tag = "2"\n}\n' doc = DocumentView.parse(hcl) results = resolve_path(doc, parse_path("*..tag[*]")) self.assertEqual(len(results), 2) @@ -243,11 +219,7 @@ def test_type_filter_attribute(self): doc = DocumentView.parse(hcl) results = resolve_path( doc, - [ - PathSegment( - name="*", select_all=True, index=None, type_filter="attribute" - ) - ], + [PathSegment(name="*", select_all=True, index=None, type_filter="attribute")], ) self.assertEqual(len(results), 1) self.assertEqual(results[0].name, "x") @@ -306,17 +278,13 @@ class TestSkipLabels(TestCase): """Test the ``~`` (skip labels) operator.""" def test_skip_labels_basic(self): - doc = DocumentView.parse( - 'resource "aws_instance" "main" {\n ami = "test"\n}\n' - ) + doc = DocumentView.parse('resource "aws_instance" "main" {\n ami = "test"\n}\n') results = resolve_path(doc, parse_path("resource~.ami")) self.assertEqual(len(results), 1) self.assertEqual(results[0].name, "ami") def test_skip_labels_wildcard(self): - doc = DocumentView.parse( - 'resource "a" "x" {\n ami = 1\n}\nresource "b" "y" {\n ami = 2\n}\n' - ) + doc = DocumentView.parse('resource "a" "x" {\n ami = 1\n}\nresource "b" "y" {\n ami = 2\n}\n') results = resolve_path(doc, parse_path("resource~[*]")) self.assertEqual(len(results), 2) diff --git a/test/unit/rules/test_abstract.py b/test/unit/rules/test_abstract.py index 3699ec0e..d2f4bb56 100644 --- a/test/unit/rules/test_abstract.py +++ b/test/unit/rules/test_abstract.py @@ -4,9 +4,8 @@ from lark import Token, Tree from lark.tree import Meta -from hcl2.rules.abstract import LarkToken, LarkRule -from hcl2.utils import SerializationOptions, SerializationContext - +from hcl2.rules.abstract import LarkRule, LarkToken +from hcl2.utils import SerializationContext, SerializationOptions # --- Concrete stubs for testing ABCs --- diff --git a/test/unit/rules/test_base.py b/test/unit/rules/test_base.py index 4dc51f92..d007d600 100644 --- a/test/unit/rules/test_base.py +++ b/test/unit/rules/test_base.py @@ -2,22 +2,21 @@ from unittest import TestCase from hcl2.const import IS_BLOCK -from hcl2.rules.base import AttributeRule, BodyRule, StartRule, BlockRule +from hcl2.rules.base import AttributeRule, BlockRule, BodyRule, StartRule from hcl2.rules.expressions import ExpressionRule, ExprTermRule from hcl2.rules.literal_rules import IdentifierRule -from hcl2.rules.strings import StringRule, StringPartRule +from hcl2.rules.strings import StringPartRule, StringRule from hcl2.rules.tokens import ( - NAME, + DBLQUOTE, EQ, LBRACE, + NAME, + NL_OR_COMMENT, RBRACE, - DBLQUOTE, STRING_CHARS, - NL_OR_COMMENT, ) from hcl2.rules.whitespace import NewLineOrCommentRule -from hcl2.utils import SerializationOptions, SerializationContext - +from hcl2.utils import SerializationContext, SerializationOptions # --- Stubs & helpers --- diff --git a/test/unit/rules/test_containers.py b/test/unit/rules/test_containers.py index 526b0216..d3b14d6d 100644 --- a/test/unit/rules/test_containers.py +++ b/test/unit/rules/test_containers.py @@ -2,33 +2,32 @@ from unittest import TestCase from hcl2.rules.containers import ( - TupleRule, - ObjectElemKeyRule, ObjectElemKeyExpressionRule, + ObjectElemKeyRule, ObjectElemRule, ObjectRule, + TupleRule, ) from hcl2.rules.expressions import ExpressionRule -from hcl2.rules.literal_rules import IdentifierRule, IntLitRule, FloatLitRule -from hcl2.rules.strings import StringRule, StringPartRule +from hcl2.rules.literal_rules import FloatLitRule, IdentifierRule, IntLitRule +from hcl2.rules.strings import StringPartRule, StringRule from hcl2.rules.tokens import ( - LSQB, - RSQB, - LBRACE, - RBRACE, - EQ, COLON, COMMA, - NAME, DBLQUOTE, + EQ, + LBRACE, + LSQB, + NAME, + NL_OR_COMMENT, + RBRACE, + RSQB, STRING_CHARS, - IntLiteral, FloatLiteral, + IntLiteral, ) from hcl2.rules.whitespace import NewLineOrCommentRule -from hcl2.rules.tokens import NL_OR_COMMENT -from hcl2.utils import SerializationOptions, SerializationContext - +from hcl2.utils import SerializationContext, SerializationOptions # --- Stubs & Helpers --- @@ -98,9 +97,7 @@ def test_elements_skips_non_expressions(self): self.assertEqual(len(rule.elements), 2) def test_serialize_default_returns_list(self): - rule = TupleRule( - [LSQB(), StubExpression(1), COMMA(), StubExpression(2), RSQB()] - ) + rule = TupleRule([LSQB(), StubExpression(1), COMMA(), StubExpression(2), RSQB()]) result = rule.serialize() self.assertEqual(result, [1, 2]) @@ -113,9 +110,7 @@ def test_serialize_single_element(self): self.assertEqual(rule.serialize(), [42]) def test_serialize_wrap_tuples(self): - rule = TupleRule( - [LSQB(), StubExpression("a"), COMMA(), StubExpression("b"), RSQB()] - ) + rule = TupleRule([LSQB(), StubExpression("a"), COMMA(), StubExpression("b"), RSQB()]) opts = SerializationOptions(wrap_tuples=True) result = rule.serialize(options=opts) self.assertEqual(result, "${[a, b]}") @@ -134,9 +129,7 @@ def test_serialize_inside_dollar_string(self): self.assertEqual(result, "[a]") def test_serialize_inside_dollar_string_no_extra_wrap(self): - rule = TupleRule( - [LSQB(), StubExpression("a"), COMMA(), StubExpression("b"), RSQB()] - ) + rule = TupleRule([LSQB(), StubExpression("a"), COMMA(), StubExpression("b"), RSQB()]) ctx = SerializationContext(inside_dollar_string=True) result = rule.serialize(context=ctx) self.assertEqual(result, "[a, b]") @@ -184,9 +177,7 @@ def test_serialize_string(self): class TestObjectElemKeyExpressionRule(TestCase): def test_lark_name(self): - self.assertEqual( - ObjectElemKeyExpressionRule.lark_name(), "object_elem_key_expr" - ) + self.assertEqual(ObjectElemKeyExpressionRule.lark_name(), "object_elem_key_expr") def test_expression_property(self): expr = StubExpression("1 + 1") diff --git a/test/unit/rules/test_directives.py b/test/unit/rules/test_directives.py index bb2be42e..54408409 100644 --- a/test/unit/rules/test_directives.py +++ b/test/unit/rules/test_directives.py @@ -4,29 +4,29 @@ from unittest import TestCase from hcl2.rules.directives import ( - TemplateIfStartRule, TemplateElseRule, + TemplateEndforRule, TemplateEndifRule, + TemplateForRule, TemplateForStartRule, - TemplateEndforRule, TemplateIfRule, - TemplateForRule, + TemplateIfStartRule, ) from hcl2.rules.literal_rules import IdentifierRule from hcl2.rules.strings import StringPartRule from hcl2.rules.tokens import ( - NAME, + COMMA, DIRECTIVE_START, - STRIP_MARKER, - RBRACE, - IF, ELSE, + ENDFOR, ENDIF, FOR, + IF, IN, - ENDFOR, - COMMA, + NAME, + RBRACE, STRING_CHARS, + STRIP_MARKER, ) @@ -41,9 +41,7 @@ def test_serialize_basic(self): def test_serialize_strip_markers(self): cond = IdentifierRule([NAME("cond")]) - rule = TemplateIfStartRule( - [DIRECTIVE_START(), STRIP_MARKER(), IF(), cond, STRIP_MARKER(), RBRACE()] - ) + rule = TemplateIfStartRule([DIRECTIVE_START(), STRIP_MARKER(), IF(), cond, STRIP_MARKER(), RBRACE()]) self.assertEqual(rule.serialize(), "%{~ if cond ~}") def test_condition_property(self): @@ -61,9 +59,7 @@ def test_serialize_basic(self): self.assertEqual(rule.serialize(), "%{ else }") def test_serialize_strip_markers(self): - rule = TemplateElseRule( - [DIRECTIVE_START(), STRIP_MARKER(), ELSE(), STRIP_MARKER(), RBRACE()] - ) + rule = TemplateElseRule([DIRECTIVE_START(), STRIP_MARKER(), ELSE(), STRIP_MARKER(), RBRACE()]) self.assertEqual(rule.serialize(), "%{~ else ~}") @@ -83,18 +79,14 @@ def test_lark_name(self): def test_serialize_basic(self): iterator = IdentifierRule([NAME("item")]) collection = IdentifierRule([NAME("items")]) - rule = TemplateForStartRule( - [DIRECTIVE_START(), FOR(), iterator, IN(), collection, RBRACE()] - ) + rule = TemplateForStartRule([DIRECTIVE_START(), FOR(), iterator, IN(), collection, RBRACE()]) self.assertEqual(rule.serialize(), "%{ for item in items }") def test_serialize_key_value(self): key = IdentifierRule([NAME("k")]) val = IdentifierRule([NAME("v")]) collection = IdentifierRule([NAME("map")]) - rule = TemplateForStartRule( - [DIRECTIVE_START(), FOR(), key, COMMA(), val, IN(), collection, RBRACE()] - ) + rule = TemplateForStartRule([DIRECTIVE_START(), FOR(), key, COMMA(), val, IN(), collection, RBRACE()]) self.assertEqual(rule.serialize(), "%{ for k, v in map }") def test_serialize_strip_markers(self): @@ -152,9 +144,7 @@ def test_serialize_strip_markers(self): [DIRECTIVE_START(), STRIP_MARKER(), IF(), cond, STRIP_MARKER(), RBRACE()] ) body = [StringPartRule([STRING_CHARS("x")])] - endif = TemplateEndifRule( - [DIRECTIVE_START(), STRIP_MARKER(), ENDIF(), STRIP_MARKER(), RBRACE()] - ) + endif = TemplateEndifRule([DIRECTIVE_START(), STRIP_MARKER(), ENDIF(), STRIP_MARKER(), RBRACE()]) rule = TemplateIfRule(if_start, body, None, None, endif) self.assertEqual(rule.serialize(), "%{~ if c ~}x%{~ endif ~}") @@ -166,9 +156,7 @@ def test_lark_name(self): def test_serialize_basic(self): iterator = IdentifierRule([NAME("item")]) collection = IdentifierRule([NAME("items")]) - for_start = TemplateForStartRule( - [DIRECTIVE_START(), FOR(), iterator, IN(), collection, RBRACE()] - ) + for_start = TemplateForStartRule([DIRECTIVE_START(), FOR(), iterator, IN(), collection, RBRACE()]) body = [StringPartRule([STRING_CHARS("text")])] endfor = TemplateEndforRule([DIRECTIVE_START(), ENDFOR(), RBRACE()]) rule = TemplateForRule(for_start, body, endfor) diff --git a/test/unit/rules/test_expressions.py b/test/unit/rules/test_expressions.py index 8ab7e8db..9b7e9a7f 100644 --- a/test/unit/rules/test_expressions.py +++ b/test/unit/rules/test_expressions.py @@ -3,24 +3,23 @@ from hcl2.rules.abstract import LarkRule from hcl2.rules.expressions import ( + BinaryOpRule, + BinaryTermRule, + ConditionalRule, ExpressionRule, ExprTermRule, - ConditionalRule, - BinaryTermRule, - BinaryOpRule, UnaryOpRule, ) from hcl2.rules.literal_rules import BinaryOperatorRule from hcl2.rules.tokens import ( + BINARY_OP, + COLON, LPAR, - RPAR, QMARK, - COLON, - BINARY_OP, + RPAR, StringToken, ) -from hcl2.utils import SerializationOptions, SerializationContext - +from hcl2.utils import SerializationContext, SerializationOptions # --- Stubs & helpers --- @@ -148,9 +147,7 @@ def test_serialize_sets_inside_parentheses_context(self): seen_context = {} class ContextCapture(ExpressionRule): - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ): + def serialize(self, options=SerializationOptions(), context=SerializationContext()): seen_context["inside_parentheses"] = context.inside_parentheses return "x" @@ -163,9 +160,7 @@ def test_serialize_no_parens_preserves_inside_parentheses(self): seen_context = {} class ContextCapture(ExpressionRule): - def serialize( - self, options=SerializationOptions(), context=SerializationContext() - ): + def serialize(self, options=SerializationOptions(), context=SerializationContext()): seen_context["inside_parentheses"] = context.inside_parentheses return "x" @@ -199,9 +194,7 @@ def test_construction_inserts_optional_slots(self): def test_condition_property(self): cond = StubExpression("cond") - rule = ConditionalRule( - [cond, QMARK(), StubExpression("t"), COLON(), StubExpression("f")] - ) + rule = ConditionalRule([cond, QMARK(), StubExpression("t"), COLON(), StubExpression("f")]) self.assertIs(rule.condition, cond) def test_if_true_property(self): diff --git a/test/unit/rules/test_for_expressions.py b/test/unit/rules/test_for_expressions.py index 38cb90ea..bf3c7fe9 100644 --- a/test/unit/rules/test_for_expressions.py +++ b/test/unit/rules/test_for_expressions.py @@ -3,28 +3,27 @@ from hcl2.rules.expressions import ExpressionRule from hcl2.rules.for_expressions import ( - ForIntroRule, ForCondRule, - ForTupleExprRule, + ForIntroRule, ForObjectExprRule, + ForTupleExprRule, ) from hcl2.rules.literal_rules import IdentifierRule from hcl2.rules.tokens import ( - NAME, - LSQB, - RSQB, - LBRACE, - RBRACE, - FOR, - IN, - IF, - COMMA, COLON, + COMMA, ELLIPSIS, + FOR, FOR_OBJECT_ARROW, + IF, + IN, + LBRACE, + LSQB, + NAME, + RBRACE, + RSQB, ) -from hcl2.utils import SerializationOptions, SerializationContext - +from hcl2.utils import SerializationContext, SerializationOptions # --- Stubs & helpers --- @@ -94,9 +93,7 @@ def test_first_iterator_single(self): def test_first_iterator_dual(self): i1 = _make_identifier("k") i2 = _make_identifier("v") - rule = ForIntroRule( - [FOR(), i1, COMMA(), i2, IN(), StubExpression("items"), COLON()] - ) + rule = ForIntroRule([FOR(), i1, COMMA(), i2, IN(), StubExpression("items"), COLON()]) self.assertIs(rule.first_iterator, i1) def test_second_iterator_none_when_single(self): @@ -405,9 +402,7 @@ def test_serialize_preserves_caller_options(self): RBRACE(), ] ) - caller_options = SerializationOptions( - with_comments=True, preserve_heredocs=False - ) + caller_options = SerializationOptions(with_comments=True, preserve_heredocs=False) rule.serialize(options=caller_options) # value_expr should receive options with wrap_objects=True but # all other caller settings preserved diff --git a/test/unit/rules/test_functions.py b/test/unit/rules/test_functions.py index 6d3146c0..5a40538b 100644 --- a/test/unit/rules/test_functions.py +++ b/test/unit/rules/test_functions.py @@ -7,9 +7,8 @@ FunctionCallRule, ) from hcl2.rules.literal_rules import IdentifierRule -from hcl2.rules.tokens import NAME, COMMA, ELLIPSIS, LPAR, RPAR, StringToken -from hcl2.utils import SerializationOptions, SerializationContext - +from hcl2.rules.tokens import COMMA, ELLIPSIS, LPAR, NAME, RPAR, StringToken +from hcl2.utils import SerializationContext, SerializationOptions # --- Stubs & helpers --- diff --git a/test/unit/rules/test_literal_rules.py b/test/unit/rules/test_literal_rules.py index 1bcdc0d2..784f3612 100644 --- a/test/unit/rules/test_literal_rules.py +++ b/test/unit/rules/test_literal_rules.py @@ -2,21 +2,21 @@ from unittest import TestCase from hcl2.rules.literal_rules import ( - KeywordRule, + BinaryOperatorRule, + FloatLitRule, IdentifierRule, IntLitRule, - FloatLitRule, - BinaryOperatorRule, + KeywordRule, LiteralValueRule, ) from hcl2.rules.tokens import ( - NAME, BINARY_OP, - IntLiteral, - FloatLiteral, - TRUE, FALSE, + NAME, NULL, + TRUE, + FloatLiteral, + IntLiteral, ) from hcl2.utils import SerializationContext, SerializationOptions diff --git a/test/unit/rules/test_strings.py b/test/unit/rules/test_strings.py index e142de5b..d5eac752 100644 --- a/test/unit/rules/test_strings.py +++ b/test/unit/rules/test_strings.py @@ -3,23 +3,22 @@ from hcl2.rules.expressions import ExpressionRule from hcl2.rules.strings import ( + HeredocTemplateRule, + HeredocTrimTemplateRule, InterpolationRule, StringPartRule, StringRule, - HeredocTemplateRule, - HeredocTrimTemplateRule, ) from hcl2.rules.tokens import ( - INTERP_START, - RBRACE, DBLQUOTE, - STRING_CHARS, ESCAPED_INTERPOLATION, HEREDOC_TEMPLATE, HEREDOC_TRIM_TEMPLATE, + INTERP_START, + RBRACE, + STRING_CHARS, ) -from hcl2.utils import SerializationOptions, SerializationContext - +from hcl2.utils import SerializationContext, SerializationOptions # --- Stubs --- diff --git a/test/unit/rules/test_tokens.py b/test/unit/rules/test_tokens.py index 0512cc51..255d22ca 100644 --- a/test/unit/rules/test_tokens.py +++ b/test/unit/rules/test_tokens.py @@ -2,22 +2,22 @@ from unittest import TestCase from hcl2.rules.tokens import ( - StringToken, - StaticStringToken, - IntLiteral, - FloatLiteral, - NAME, - LPAR, - RPAR, - QMARK, COLON, - EQ, - DOT, COMMA, + DOT, + EQ, LBRACE, - RBRACE, + LPAR, LSQB, + NAME, + QMARK, + RBRACE, + RPAR, RSQB, + FloatLiteral, + IntLiteral, + StaticStringToken, + StringToken, ) diff --git a/test/unit/rules/test_whitespace.py b/test/unit/rules/test_whitespace.py index d182b789..bb6410c7 100644 --- a/test/unit/rules/test_whitespace.py +++ b/test/unit/rules/test_whitespace.py @@ -2,9 +2,8 @@ from unittest import TestCase from hcl2.rules.tokens import NAME, NL_OR_COMMENT -from hcl2.rules.whitespace import NewLineOrCommentRule, InlineCommentMixIn -from hcl2.utils import SerializationOptions, SerializationContext - +from hcl2.rules.whitespace import InlineCommentMixIn, NewLineOrCommentRule +from hcl2.utils import SerializationContext, SerializationOptions # --- Concrete stub for testing InlineCommentMixIn --- diff --git a/test/unit/test_api.py b/test/unit/test_api.py index c39e6ae4..6af029a5 100644 --- a/test/unit/test_api.py +++ b/test/unit/test_api.py @@ -5,27 +5,26 @@ from lark.tree import Tree from hcl2.api import ( - load, - loads, dump, dumps, + from_dict, + from_json, + load, + loads, parse, - parses, parse_to_tree, + parses, parses_to_tree, - from_dict, - from_json, + query, reconstruct, - transform, serialize, - query, + transform, ) from hcl2.deserializer import DeserializerOptions from hcl2.formatter import FormatterOptions from hcl2.rules.base import StartRule from hcl2.utils import SerializationOptions - SIMPLE_HCL = "x = 5\n" SIMPLE_DICT = {"x": 5} @@ -42,16 +41,12 @@ def test_returns_dict(self): self.assertIsInstance(result, dict) def test_with_serialization_options(self): - result = loads( - SIMPLE_HCL, serialization_options=SerializationOptions(with_comments=False) - ) + result = loads(SIMPLE_HCL, serialization_options=SerializationOptions(with_comments=False)) self.assertIsInstance(result, dict) self.assertEqual(result["x"], 5) def test_with_meta_option(self): - result = loads( - BLOCK_HCL, serialization_options=SerializationOptions(with_meta=True) - ) + result = loads(BLOCK_HCL, serialization_options=SerializationOptions(with_meta=True)) self.assertIn("resource", result) # Verify the option is accepted and produces a dict with expected content self.assertIsInstance(result, dict) @@ -63,9 +58,7 @@ def test_block_parsing(self): def test_strip_string_quotes(self): result = loads( BLOCK_HCL, - serialization_options=SerializationOptions( - strip_string_quotes=True, explicit_blocks=False - ), + serialization_options=SerializationOptions(strip_string_quotes=True, explicit_blocks=False), ) resource_list = result["resource"] self.assertEqual(len(resource_list), 1) @@ -89,9 +82,7 @@ def test_from_file(self): def test_with_serialization_options(self): f = StringIO(SIMPLE_HCL) - result = load( - f, serialization_options=SerializationOptions(with_comments=False) - ) + result = load(f, serialization_options=SerializationOptions(with_comments=False)) self.assertEqual(result["x"], 5) @@ -199,9 +190,7 @@ def test_returns_dict(self): def test_with_options(self): tree = parses(SIMPLE_HCL) - result = serialize( - tree, serialization_options=SerializationOptions(with_comments=False) - ) + result = serialize(tree, serialization_options=SerializationOptions(with_comments=False)) self.assertIsInstance(result, dict) def test_none_options_uses_defaults(self): diff --git a/test/unit/test_deserializer.py b/test/unit/test_deserializer.py index cfaa0ae7..25a88df6 100644 --- a/test/unit/test_deserializer.py +++ b/test/unit/test_deserializer.py @@ -1,38 +1,37 @@ # pylint: disable=C0103,C0114,C0115,C0116 from unittest import TestCase -from hcl2.const import IS_BLOCK, COMMENTS_KEY, INLINE_COMMENTS_KEY +from hcl2.const import COMMENTS_KEY, INLINE_COMMENTS_KEY, IS_BLOCK from hcl2.deserializer import BaseDeserializer, DeserializerOptions -from hcl2.rules.base import StartRule, BodyRule, BlockRule, AttributeRule +from hcl2.rules.base import AttributeRule, BlockRule, BodyRule, StartRule from hcl2.rules.containers import ( - TupleRule, - ObjectRule, - ObjectElemRule, ObjectElemKeyExpressionRule, + ObjectElemRule, + ObjectRule, + TupleRule, ) from hcl2.rules.expressions import ExprTermRule from hcl2.rules.literal_rules import ( + FloatLitRule, IdentifierRule, IntLitRule, - FloatLitRule, LiteralValueRule, ) from hcl2.rules.strings import ( - StringRule, - StringPartRule, - InterpolationRule, HeredocTemplateRule, HeredocTrimTemplateRule, + InterpolationRule, + StringPartRule, + StringRule, ) from hcl2.rules.tokens import ( - STRING_CHARS, - ESCAPED_INTERPOLATION, + COLON, COMMA, EQ, - COLON, + ESCAPED_INTERPOLATION, + STRING_CHARS, ) - # --- helpers --- diff --git a/test/unit/test_formatter.py b/test/unit/test_formatter.py index 34fadc71..a774fb1b 100644 --- a/test/unit/test_formatter.py +++ b/test/unit/test_formatter.py @@ -3,43 +3,42 @@ from hcl2.formatter import BaseFormatter, FormatterOptions from hcl2.rules.base import ( - StartRule, - BodyRule, - BlockRule, AttributeRule, + BlockRule, + BodyRule, + StartRule, ) from hcl2.rules.containers import ( - ObjectRule, - ObjectElemRule, ObjectElemKeyRule, + ObjectElemRule, + ObjectRule, TupleRule, ) from hcl2.rules.expressions import ExprTermRule from hcl2.rules.for_expressions import ( - ForIntroRule, ForCondRule, - ForTupleExprRule, + ForIntroRule, ForObjectExprRule, + ForTupleExprRule, ) from hcl2.rules.literal_rules import IdentifierRule from hcl2.rules.tokens import ( - NAME, + COLON, + COMMA, + ELLIPSIS, EQ, + FOR, + FOR_OBJECT_ARROW, + IF, + IN, LBRACE, - RBRACE, LSQB, + NAME, + RBRACE, RSQB, - COMMA, - COLON, - FOR, - IN, - IF, - ELLIPSIS, - FOR_OBJECT_ARROW, ) from hcl2.rules.whitespace import NewLineOrCommentRule - # --- helpers --- @@ -211,9 +210,7 @@ def test_body_with_single_attribute(self): f.format_body_rule(body, 1) # Should have: newline, attr, (final newline removed by pop) - nlc_children = [ - c for c in body._children if isinstance(c, NewLineOrCommentRule) - ] + nlc_children = [c for c in body._children if isinstance(c, NewLineOrCommentRule)] self.assertGreaterEqual(len(nlc_children), 1) # The attribute should still be in children attr_children = [c for c in body._children if isinstance(c, AttributeRule)] @@ -280,9 +277,7 @@ def test_empty_block_open_false(self): f.format_block_rule(block, indent_level=1) # Should NOT insert newline before RBRACE - nlc_children = [ - c for c in block.children if isinstance(c, NewLineOrCommentRule) - ] + nlc_children = [c for c in block.children if isinstance(c, NewLineOrCommentRule)] # Only the body formatting newlines, but no double-newline insertion has_double_nl = any(_nlc_value(c).startswith("\n\n") for c in nlc_children) self.assertFalse(has_double_nl) @@ -386,9 +381,7 @@ def test_multiple_elements_get_newlines_between(self): f.format_object_rule(obj, indent_level=1) # Should have newlines between the elements nlc_count = sum(1 for c in obj._children if isinstance(c, NewLineOrCommentRule)) - self.assertGreaterEqual( - nlc_count, 3 - ) # after LBRACE, between elems, before RBRACE + self.assertGreaterEqual(nlc_count, 3) # after LBRACE, between elems, before RBRACE # --- format_expression dispatch --- @@ -516,9 +509,7 @@ def test_format_body_uses_indent_length(self): body._parent = block f.format_body_rule(body, 1) - nlc_children = [ - c for c in body._children if isinstance(c, NewLineOrCommentRule) - ] + nlc_children = [c for c in body._children if isinstance(c, NewLineOrCommentRule)] # At least one newline should have 4 spaces of indent has_4_space = any(" " in _nlc_value(c) for c in nlc_children) self.assertTrue(has_4_space) @@ -774,9 +765,7 @@ def test_nested_value_tuple_formatting(self): f.format_forobjectexpr(expr, indent_level=1) - nlc_count = sum( - 1 for c in inner_tup._children if isinstance(c, NewLineOrCommentRule) - ) + nlc_count = sum(1 for c in inner_tup._children if isinstance(c, NewLineOrCommentRule)) self.assertGreater(nlc_count, 0) def test_for_cond_expression_formatting(self): diff --git a/test/unit/test_reconstructor.py b/test/unit/test_reconstructor.py index e9f46900..afac9690 100644 --- a/test/unit/test_reconstructor.py +++ b/test/unit/test_reconstructor.py @@ -3,54 +3,53 @@ from unittest import TestCase -from lark import Tree, Token +from lark import Token, Tree from hcl2.reconstructor import HCLReconstructor -from hcl2.rules.base import BlockRule, AttributeRule, BodyRule, StartRule +from hcl2.rules.base import AttributeRule, BlockRule, BodyRule, StartRule from hcl2.rules.containers import ( - ObjectRule, - ObjectElemRule, ObjectElemKeyRule, + ObjectElemRule, + ObjectRule, TupleRule, ) from hcl2.rules.expressions import ( + BinaryOpRule, BinaryTermRule, - ExprTermRule, ConditionalRule, + ExprTermRule, UnaryOpRule, - BinaryOpRule, ) from hcl2.rules.for_expressions import ( - ForIntroRule, ForCondRule, - ForTupleExprRule, + ForIntroRule, ForObjectExprRule, + ForTupleExprRule, ) from hcl2.rules.literal_rules import BinaryOperatorRule, IdentifierRule from hcl2.rules.strings import StringRule from hcl2.rules.tokens import ( - NAME, - NL_OR_COMMENT, + BINARY_OP, + COLON, + COMMA, + DBLQUOTE, + ELLIPSIS, EQ, + FOR, + FOR_OBJECT_ARROW, + IF, + IN, LBRACE, - RBRACE, LSQB, - RSQB, - COMMA, - COLON, + NAME, + NL_OR_COMMENT, QMARK, - FOR, - IN, - IF, - ELLIPSIS, - FOR_OBJECT_ARROW, - DBLQUOTE, + RBRACE, + RSQB, STRING_CHARS, - BINARY_OP, ) from hcl2.rules.whitespace import NewLineOrCommentRule - # --- helpers --- @@ -218,9 +217,7 @@ def test_space_before_lbrace_in_block(self): r._last_was_space = False r._last_token_name = "NAME" token = Token("LBRACE", "{") - self.assertTrue( - r._should_add_space_before(token, parent_rule_name=BlockRule.lark_name()) - ) + self.assertTrue(r._should_add_space_before(token, parent_rule_name=BlockRule.lark_name())) def test_no_space_before_lbrace_outside_block(self): r = _r() @@ -294,9 +291,7 @@ def test_no_space_in_unary_op(self): r._last_was_space = False r._last_token_name = "MINUS" token = Token("NAME", "x") - self.assertFalse( - r._should_add_space_before(token, parent_rule_name=UnaryOpRule.lark_name()) - ) + self.assertFalse(r._should_add_space_before(token, parent_rule_name=UnaryOpRule.lark_name())) class TestSpaceAroundConditional(TestCase): @@ -305,44 +300,28 @@ def test_space_before_qmark(self): r._last_was_space = False r._last_token_name = "NAME" token = Token("QMARK", "?") - self.assertTrue( - r._should_add_space_before( - token, parent_rule_name=ConditionalRule.lark_name() - ) - ) + self.assertTrue(r._should_add_space_before(token, parent_rule_name=ConditionalRule.lark_name())) def test_space_before_colon(self): r = _r() r._last_was_space = False r._last_token_name = "NAME" token = Token("COLON", ":") - self.assertTrue( - r._should_add_space_before( - token, parent_rule_name=ConditionalRule.lark_name() - ) - ) + self.assertTrue(r._should_add_space_before(token, parent_rule_name=ConditionalRule.lark_name())) def test_space_after_qmark(self): r = _r() r._last_was_space = False r._last_token_name = "QMARK" token = Token("NAME", "x") - self.assertTrue( - r._should_add_space_before( - token, parent_rule_name=ConditionalRule.lark_name() - ) - ) + self.assertTrue(r._should_add_space_before(token, parent_rule_name=ConditionalRule.lark_name())) def test_space_after_colon(self): r = _r() r._last_was_space = False r._last_token_name = "COLON" token = Token("NAME", "x") - self.assertTrue( - r._should_add_space_before( - token, parent_rule_name=ConditionalRule.lark_name() - ) - ) + self.assertTrue(r._should_add_space_before(token, parent_rule_name=ConditionalRule.lark_name())) def test_no_space_qmark_outside_conditional(self): r = _r() @@ -475,9 +454,7 @@ def test_space_before_colon_in_for_intro(self): r._last_was_space = False r._last_token_name = "NAME" token = Token("COLON", ":") - self.assertTrue( - r._should_add_space_before(token, parent_rule_name=ForIntroRule.lark_name()) - ) + self.assertTrue(r._should_add_space_before(token, parent_rule_name=ForIntroRule.lark_name())) def test_no_space_colon_outside_for_intro_and_conditional(self): r = _r() @@ -497,9 +474,7 @@ def test_space_between_labels_in_block(self): r._last_token_name = "NAME" r._last_rule_name = IdentifierRule.lark_name() tree = Tree(IdentifierRule.lark_name(), [Token("NAME", "label2")]) - self.assertTrue( - r._should_add_space_before(tree, parent_rule_name=BlockRule.lark_name()) - ) + self.assertTrue(r._should_add_space_before(tree, parent_rule_name=BlockRule.lark_name())) def test_space_between_string_and_identifier_in_block(self): r = _r() @@ -507,9 +482,7 @@ def test_space_between_string_and_identifier_in_block(self): r._last_token_name = "DBLQUOTE" r._last_rule_name = StringRule.lark_name() tree = Tree(IdentifierRule.lark_name(), [Token("NAME", "label")]) - self.assertTrue( - r._should_add_space_before(tree, parent_rule_name=BlockRule.lark_name()) - ) + self.assertTrue(r._should_add_space_before(tree, parent_rule_name=BlockRule.lark_name())) def test_no_space_between_labels_outside_block(self): r = _r() @@ -525,9 +498,7 @@ def test_no_space_for_non_label_tree_in_block(self): r._last_token_name = "NAME" r._last_rule_name = StringRule.lark_name() self.assertFalse( - r._should_add_space_before( - Tree("expr_term", []), parent_rule_name=BlockRule.lark_name() - ) + r._should_add_space_before(Tree("expr_term", []), parent_rule_name=BlockRule.lark_name()) ) def test_space_after_qmark_before_tree_in_conditional(self): @@ -535,31 +506,21 @@ def test_space_after_qmark_before_tree_in_conditional(self): r._last_was_space = False r._last_token_name = "QMARK" tree = Tree("expr_term", [Token("NAME", "x")]) - self.assertTrue( - r._should_add_space_before( - tree, parent_rule_name=ConditionalRule.lark_name() - ) - ) + self.assertTrue(r._should_add_space_before(tree, parent_rule_name=ConditionalRule.lark_name())) def test_space_after_colon_before_tree_in_conditional(self): r = _r() r._last_was_space = False r._last_token_name = "COLON" tree = Tree("expr_term", [Token("NAME", "x")]) - self.assertTrue( - r._should_add_space_before( - tree, parent_rule_name=ConditionalRule.lark_name() - ) - ) + self.assertTrue(r._should_add_space_before(tree, parent_rule_name=ConditionalRule.lark_name())) def test_no_space_after_other_token_before_tree_in_conditional(self): r = _r() r._last_was_space = False r._last_token_name = "LPAR" self.assertFalse( - r._should_add_space_before( - Tree("expr_term", []), parent_rule_name=ConditionalRule.lark_name() - ) + r._should_add_space_before(Tree("expr_term", []), parent_rule_name=ConditionalRule.lark_name()) ) def test_space_after_colon_before_tree_in_for_tuple_expr(self): @@ -567,33 +528,21 @@ def test_space_after_colon_before_tree_in_for_tuple_expr(self): r._last_was_space = False r._last_token_name = "COLON" tree = Tree("expr_term", [Token("NAME", "x")]) - self.assertTrue( - r._should_add_space_before( - tree, parent_rule_name=ForTupleExprRule.lark_name() - ) - ) + self.assertTrue(r._should_add_space_before(tree, parent_rule_name=ForTupleExprRule.lark_name())) def test_space_after_colon_before_tree_in_for_object_expr(self): r = _r() r._last_was_space = False r._last_token_name = "COLON" tree = Tree("expr_term", [Token("NAME", "x")]) - self.assertTrue( - r._should_add_space_before( - tree, parent_rule_name=ForObjectExprRule.lark_name() - ) - ) + self.assertTrue(r._should_add_space_before(tree, parent_rule_name=ForObjectExprRule.lark_name())) def test_no_space_after_colon_before_nlc_in_for_expr(self): r = _r() r._last_was_space = False r._last_token_name = "COLON" tree = Tree("new_line_or_comment", [Token("NL_OR_COMMENT", "\n")]) - self.assertFalse( - r._should_add_space_before( - tree, parent_rule_name=ForTupleExprRule.lark_name() - ) - ) + self.assertFalse(r._should_add_space_before(tree, parent_rule_name=ForTupleExprRule.lark_name())) def test_no_space_after_colon_outside_for_expr(self): r = _r() @@ -856,9 +805,7 @@ def test_addition_raw_lark_tree(self): Tree( "expr_term", [ - Tree( - "identifier", [Token("NAME", "a")] - ), + Tree("identifier", [Token("NAME", "a")]), ], ), Tree( diff --git a/test/unit/test_utils.py b/test/unit/test_utils.py index 01954113..ab5335c5 100644 --- a/test/unit/test_utils.py +++ b/test/unit/test_utils.py @@ -2,8 +2,8 @@ from unittest import TestCase from hcl2.utils import ( - SerializationOptions, SerializationContext, + SerializationOptions, is_dollar_string, to_dollar_string, unwrap_dollar_string, diff --git a/test/unit/test_walk.py b/test/unit/test_walk.py index ec81bb48..ae718c0d 100644 --- a/test/unit/test_walk.py +++ b/test/unit/test_walk.py @@ -4,9 +4,9 @@ from hcl2.rules.base import AttributeRule, BlockRule, BodyRule, StartRule from hcl2.rules.expressions import ExpressionRule, ExprTermRule from hcl2.rules.literal_rules import IdentifierRule -from hcl2.rules.tokens import NAME, EQ, LBRACE, RBRACE, NL_OR_COMMENT +from hcl2.rules.tokens import EQ, LBRACE, NAME, NL_OR_COMMENT, RBRACE from hcl2.rules.whitespace import NewLineOrCommentRule -from hcl2.utils import SerializationOptions, SerializationContext +from hcl2.utils import SerializationContext, SerializationOptions from hcl2.walk import ( ancestors, find_all, @@ -104,9 +104,7 @@ def test_finds_all_attributes(self): def test_finds_nested(self): BodyRule([_make_attribute("inner", 1)]) # unused but creates parent refs - block = _make_block( - [_make_identifier("resource")], [_make_attribute("outer", 2)] - ) + block = _make_block([_make_identifier("resource")], [_make_attribute("outer", 2)]) outer_body = BodyRule([block]) start = StartRule([outer_body]) attrs = list(find_all(start, AttributeRule)) @@ -143,8 +141,7 @@ def test_predicate(self): found = list( find_by_predicate( body, - lambda n: isinstance(n, AttributeRule) - and n.identifier.serialize() == "x", + lambda n: isinstance(n, AttributeRule) and n.identifier.serialize() == "x", ) ) self.assertEqual(len(found), 1) From 1da8f016897fdb6220913e9e5edc4310b56a9830 Mon Sep 17 00:00:00 2001 From: jconstance Date: Thu, 16 Jul 2026 11:05:21 -0400 Subject: [PATCH 2/2] AT-14951: pin mypy hook language_version to fix typed-ast build mirrors-mypy v0.991's mypy build depends on typed-ast, which has no wheel for Python 3.12+ and fails to build on any current interpreter -- this was silently blocking mypy from running at all, which is why the initial migration commit needed --no-verify. With this pin, mypy now runs and passes cleanly via `pre-commit run --all-files`, no bypass needed. Applying the same fix consistently across every repo in the AT-14951 fleet that hit this issue. Co-Authored-By: Claude Sonnet 5 --- .pre-commit-config.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 92ece64a..7d6d8494 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,3 +36,7 @@ repos: rev: v0.991 # Use the sha / tag you want to point at hooks: - id: mypy + # 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