from __future__ import print_function import argparse import difflib import io import json import sys from collections import OrderedDict from typing import List from typing import Mapping from typing import Optional from typing import Sequence from typing import Tuple from typing import Union from six import text_type def _get_pretty_format( contents, indent, ensure_ascii=True, sort_keys=True, top_keys=(), ): # type: (str, str, bool, bool, Sequence[str]) -> str def pairs_first(pairs): # type: (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 = sorted(after, key=lambda x: x[0]) return OrderedDict(before + after) json_pretty = json.dumps( json.loads(contents, object_pairs_hook=pairs_first), indent=indent, ensure_ascii=ensure_ascii, # Workaround for https://bugs.python.org/issue16333 separators=(',', ': '), ) # Ensure unicode (Py2) and add the newline that dumps does not end with. return text_type(json_pretty) + '\n' def _autofix(filename, new_contents): # type: (str, str) -> None print('Fixing file {}'.format(filename)) with io.open(filename, 'w', encoding='UTF-8') as f: f.write(new_contents) def parse_num_to_int(s): # type: (str) -> Union[int, str] """Convert string numbers to int, leaving strings as is.""" try: return int(s) except ValueError: return s def parse_topkeys(s): # type: (str) -> List[str] return s.split(',') def get_diff(source, target): # type: (str, str) -> str source_lines = source.splitlines(True) target_lines = target.splitlines(True) diff = ''.join(difflib.ndiff(source_lines, target_lines)) print(diff) return diff def main(argv=None): # type: (Optional[Sequence[str]]) -> 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 io.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, ) if contents != pretty_contents: print('File {} is not pretty-formatted'.format(json_file)) if args.autofix: _autofix(json_file, pretty_contents) else: print(get_diff(''.join(contents), pretty_contents)) status = 1 except ValueError: print( 'Input File {} is not a valid JSON, consider using check-json' .format(json_file), ) return 1 return status if __name__ == '__main__': sys.exit(main())