| from __future__ import annotations |
| |
| import argparse |
| import json |
| import sys |
| from difflib import unified_diff |
| from typing import Mapping |
| from typing import Sequence |
| |
| |
| def _get_pretty_format( |
| contents: str, |
| indent: str, |
| ensure_ascii: bool = True, |
| sort_keys: bool = True, |
| top_keys: Sequence[str] = (), |
| ) -> str: |
| def pairs_first(pairs: Sequence[tuple[str, str]]) -> Mapping[str, str]: |
| before = [pair for pair in pairs if pair[0] in top_keys] |
| before = sorted(before, key=lambda x: top_keys.index(x[0])) |
| after = [pair for pair in pairs if pair[0] not in top_keys] |
| if sort_keys: |
| after.sort() |
| return dict(before + after) |
| json_pretty = json.dumps( |
| json.loads(contents, object_pairs_hook=pairs_first), |
| indent=indent, |
| ensure_ascii=ensure_ascii, |
| ) |
| return f'{json_pretty}\n' |
| |
| |
| def _autofix(filename: str, new_contents: str) -> None: |
| print(f'Fixing file {filename}') |
| with open(filename, 'w', encoding='UTF-8') as f: |
| f.write(new_contents) |
| |
| |
| def parse_num_to_int(s: str) -> int | str: |
| """Convert string numbers to int, leaving strings as is.""" |
| try: |
| return int(s) |
| except ValueError: |
| return s |
| |
| |
| def parse_topkeys(s: str) -> list[str]: |
| return s.split(',') |
| |
| |
| def get_diff(source: str, target: str, file: str) -> str: |
| source_lines = source.splitlines(True) |
| target_lines = target.splitlines(True) |
| diff = unified_diff(source_lines, target_lines, fromfile=file, tofile=file) |
| return ''.join(diff) |
| |
| |
| def main(argv: Sequence[str] | None = None) -> int: |
| parser = argparse.ArgumentParser() |
| parser.add_argument( |
| '--autofix', |
| action='store_true', |
| dest='autofix', |
| help='Automatically fixes encountered not-pretty-formatted files', |
| ) |
| parser.add_argument( |
| '--indent', |
| type=parse_num_to_int, |
| default='2', |
| help=( |
| 'The number of indent spaces or a string to be used as delimiter' |
| ' for indentation level e.g. 4 or "\t" (Default: 2)' |
| ), |
| ) |
| parser.add_argument( |
| '--no-ensure-ascii', |
| action='store_true', |
| dest='no_ensure_ascii', |
| default=False, |
| help=( |
| 'Do NOT convert non-ASCII characters to Unicode escape sequences ' |
| '(\\uXXXX)' |
| ), |
| ) |
| parser.add_argument( |
| '--no-sort-keys', |
| action='store_true', |
| dest='no_sort_keys', |
| default=False, |
| help='Keep JSON nodes in the same order', |
| ) |
| parser.add_argument( |
| '--top-keys', |
| type=parse_topkeys, |
| dest='top_keys', |
| default=[], |
| help='Ordered list of keys to keep at the top of JSON hashes', |
| ) |
| parser.add_argument('filenames', nargs='*', help='Filenames to fix') |
| args = parser.parse_args(argv) |
| |
| status = 0 |
| |
| for json_file in args.filenames: |
| with open(json_file, encoding='UTF-8') as f: |
| contents = f.read() |
| |
| try: |
| pretty_contents = _get_pretty_format( |
| contents, args.indent, ensure_ascii=not args.no_ensure_ascii, |
| sort_keys=not args.no_sort_keys, top_keys=args.top_keys, |
| ) |
| except ValueError: |
| print( |
| f'Input File {json_file} is not a valid JSON, consider using ' |
| f'check-json', |
| ) |
| status = 1 |
| else: |
| if contents != pretty_contents: |
| if args.autofix: |
| _autofix(json_file, pretty_contents) |
| else: |
| diff_output = get_diff( |
| contents, |
| pretty_contents, |
| json_file, |
| ) |
| sys.stdout.buffer.write(diff_output.encode()) |
| |
| status = 1 |
| |
| return status |
| |
| |
| if __name__ == '__main__': |
| raise SystemExit(main()) |