#!/usr/bin/env python3 """A tiny JSON pretty-printer.""" from __future__ import annotations import argparse import json import sys import unittest from pathlib import Path from typing import Any def read_json(path: str | None) -> Any: """Read JSON from a file path, or stdin when no path is given.""" if path is None or path == "-": return json.load(sys.stdin) with Path(path).open("r", encoding="utf-8") as handle: return json.load(handle) def pretty_json(value: Any, indent: int = 2, sort_keys: bool = False) -> str: """Return formatted JSON text.""" return json.dumps( value, indent=indent, sort_keys=sort_keys, ensure_ascii=False, ) + "\n" def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Pretty-print JSON.") parser.add_argument("path", nargs="?", help="JSON file to read, or stdin if omitted") parser.add_argument("-i", "--indent", type=int, default=2, help="spaces per indent level") parser.add_argument("-s", "--sort-keys", action="store_true", help="sort object keys") parser.add_argument("--test", action="store_true", help="run tests and exit") return parser class PrettyJsonTests(unittest.TestCase): def test_pretty_json_formats_nested_values(self) -> None: data: dict[str, Any] = {"name": "Ada", "items": [1, {"ok": True}]} expected = '{\n "name": "Ada",\n "items": [\n 1,\n {\n "ok": true\n }\n ]\n}\n' self.assertEqual(pretty_json(data), expected) def test_pretty_json_can_sort_keys(self) -> None: self.assertEqual(pretty_json({"b": 1, "a": 2}, sort_keys=True), '{\n "a": 2,\n "b": 1\n}\n') def run_tests() -> int: result = unittest.TextTestRunner(verbosity=2).run( unittest.defaultTestLoader.loadTestsFromTestCase(PrettyJsonTests) ) return 0 if result.wasSuccessful() else 1 def main() -> int: args = build_parser().parse_args() if args.test: return run_tests() try: data = read_json(args.path) sys.stdout.write(pretty_json(data, indent=args.indent, sort_keys=args.sort_keys)) except (OSError, json.JSONDecodeError) as exc: print(f"error: {exc}", file=sys.stderr) return 1 return 0 if __name__ == "__main__": raise SystemExit(main())