blob: 627a11cc4d3472abffac3125205b3917552d2f3d [file] [log] [blame]
Anthony Sottile8f615292022-01-15 19:24:05 -05001from __future__ import annotations
2
Léo Cavaillé55bf22d2015-06-10 17:08:48 -04003import argparse
Calum Lind00974ef2017-12-10 08:57:34 +00004import json
jack11421de4fe62021-03-14 18:21:48 +01005import sys
Joey Pinhasec6c39e2019-09-24 15:42:24 -04006from difflib import unified_diff
Anthony Sottile030bfac2019-01-31 19:19:10 -08007from typing import Mapping
Anthony Sottile030bfac2019-01-31 19:19:10 -08008from typing import Sequence
mattclegg700b18e2016-04-14 09:30:42 +01009
dmlb2000845a3d52016-11-03 09:41:23 -070010
Anthony Sottile45756522019-02-11 19:56:15 -080011def _get_pretty_format(
Anthony Sottilef5c42a02020-02-05 11:10:42 -080012 contents: str,
13 indent: str,
14 ensure_ascii: bool = True,
15 sort_keys: bool = True,
16 top_keys: Sequence[str] = (),
17) -> str:
Anthony Sottile8f615292022-01-15 19:24:05 -050018 def pairs_first(pairs: Sequence[tuple[str, str]]) -> Mapping[str, str]:
dmlb2000d06a5152016-11-03 15:47:21 -070019 before = [pair for pair in pairs if pair[0] in top_keys]
20 before = sorted(before, key=lambda x: top_keys.index(x[0]))
21 after = [pair for pair in pairs if pair[0] not in top_keys]
22 if sort_keys:
Anthony Sottilef5c42a02020-02-05 11:10:42 -080023 after.sort()
24 return dict(before + after)
Calum Lind00974ef2017-12-10 08:57:34 +000025 json_pretty = json.dumps(
26 json.loads(contents, object_pairs_hook=pairs_first),
John Hu543c5c72017-03-16 11:27:34 +080027 indent=indent,
Anthony Sottileb95dcad2017-03-20 08:24:58 -070028 ensure_ascii=ensure_ascii,
Calum Lind00974ef2017-12-10 08:57:34 +000029 )
Anthony Sottilef5c42a02020-02-05 11:10:42 -080030 return f'{json_pretty}\n'
Léo Cavaillé55bf22d2015-06-10 17:08:48 -040031
dmlb2000c7ab1972016-11-03 15:49:04 -070032
Anthony Sottilef5c42a02020-02-05 11:10:42 -080033def _autofix(filename: str, new_contents: str) -> None:
34 print(f'Fixing file {filename}')
35 with open(filename, 'w', encoding='UTF-8') as f:
Léo Cavaillé55bf22d2015-06-10 17:08:48 -040036 f.write(new_contents)
37
38
Anthony Sottile8f615292022-01-15 19:24:05 -050039def parse_num_to_int(s: str) -> int | str:
Calum Lind5b6ddaf2017-12-10 09:34:36 +000040 """Convert string numbers to int, leaving strings as is."""
Sander Maijersa5628862016-06-10 20:16:00 +020041 try:
Calum Lind5b6ddaf2017-12-10 09:34:36 +000042 return int(s)
Sander Maijersabaf0d12016-06-13 11:34:55 +020043 except ValueError:
Calum Lind5b6ddaf2017-12-10 09:34:36 +000044 return s
Sander Maijersa5628862016-06-10 20:16:00 +020045
dmlb200084b1fb62016-11-03 15:54:48 -070046
Anthony Sottile8f615292022-01-15 19:24:05 -050047def parse_topkeys(s: str) -> list[str]:
dmlb2000845a3d52016-11-03 09:41:23 -070048 return s.split(',')
Sander Maijersa5628862016-06-10 20:16:00 +020049
dmlb200084b1fb62016-11-03 15:54:48 -070050
Anthony Sottilef5c42a02020-02-05 11:10:42 -080051def get_diff(source: str, target: str, file: str) -> str:
Joey Pinhas0ff23d42019-09-13 14:30:52 -040052 source_lines = source.splitlines(True)
53 target_lines = target.splitlines(True)
Joey Pinhasec6c39e2019-09-24 15:42:24 -040054 diff = unified_diff(source_lines, target_lines, fromfile=file, tofile=file)
55 return ''.join(diff)
Joey Pinhas780f2022019-08-16 12:38:41 -040056
57
Anthony Sottile8f615292022-01-15 19:24:05 -050058def main(argv: Sequence[str] | None = None) -> int:
Léo Cavaillé55bf22d2015-06-10 17:08:48 -040059 parser = argparse.ArgumentParser()
60 parser.add_argument(
61 '--autofix',
62 action='store_true',
63 dest='autofix',
Anthony Sottile17478a02016-04-14 08:25:52 -070064 help='Automatically fixes encountered not-pretty-formatted files',
Léo Cavaillé55bf22d2015-06-10 17:08:48 -040065 )
66 parser.add_argument(
67 '--indent',
Calum Lind5b6ddaf2017-12-10 09:34:36 +000068 type=parse_num_to_int,
69 default='2',
70 help=(
71 'The number of indent spaces or a string to be used as delimiter'
72 ' for indentation level e.g. 4 or "\t" (Default: 2)'
73 ),
Léo Cavaillé55bf22d2015-06-10 17:08:48 -040074 )
Sébastien Larivièref769c202016-03-12 17:04:33 -050075 parser.add_argument(
John Hu543c5c72017-03-16 11:27:34 +080076 '--no-ensure-ascii',
77 action='store_true',
78 dest='no_ensure_ascii',
79 default=False,
Anthony Sottile45756522019-02-11 19:56:15 -080080 help=(
81 'Do NOT convert non-ASCII characters to Unicode escape sequences '
82 '(\\uXXXX)'
83 ),
John Hu543c5c72017-03-16 11:27:34 +080084 )
85 parser.add_argument(
Sébastien Larivièref769c202016-03-12 17:04:33 -050086 '--no-sort-keys',
87 action='store_true',
88 dest='no_sort_keys',
89 default=False,
Anthony Sottile17478a02016-04-14 08:25:52 -070090 help='Keep JSON nodes in the same order',
Sébastien Larivièref769c202016-03-12 17:04:33 -050091 )
dmlb2000845a3d52016-11-03 09:41:23 -070092 parser.add_argument(
93 '--top-keys',
94 type=parse_topkeys,
95 dest='top_keys',
96 default=[],
97 help='Ordered list of keys to keep at the top of JSON hashes',
98 )
Léo Cavaillé55bf22d2015-06-10 17:08:48 -040099 parser.add_argument('filenames', nargs='*', help='Filenames to fix')
100 args = parser.parse_args(argv)
101
102 status = 0
103
104 for json_file in args.filenames:
Anthony Sottilef5c42a02020-02-05 11:10:42 -0800105 with open(json_file, encoding='UTF-8') as f:
Léo Cavaillé55bf22d2015-06-10 17:08:48 -0400106 contents = f.read()
Léo Cavaillé55bf22d2015-06-10 17:08:48 -0400107
Anthony Sottile17478a02016-04-14 08:25:52 -0700108 try:
109 pretty_contents = _get_pretty_format(
John Hu543c5c72017-03-16 11:27:34 +0800110 contents, args.indent, ensure_ascii=not args.no_ensure_ascii,
Anthony Sottileb95dcad2017-03-20 08:24:58 -0700111 sort_keys=not args.no_sort_keys, top_keys=args.top_keys,
Anthony Sottile17478a02016-04-14 08:25:52 -0700112 )
Calum Lind00974ef2017-12-10 08:57:34 +0000113 except ValueError:
Léo Cavaillé55bf22d2015-06-10 17:08:48 -0400114 print(
Anthony Sottilef5c42a02020-02-05 11:10:42 -0800115 f'Input File {json_file} is not a valid JSON, consider using '
116 f'check-json',
Léo Cavaillé55bf22d2015-06-10 17:08:48 -0400117 )
118 return 1
119
jack11421de4fe62021-03-14 18:21:48 +0100120 if contents != pretty_contents:
121 if args.autofix:
122 _autofix(json_file, pretty_contents)
123 else:
124 diff_output = get_diff(contents, pretty_contents, json_file)
125 sys.stdout.buffer.write(diff_output.encode())
126
127 status = 1
128
Léo Cavaillé55bf22d2015-06-10 17:08:48 -0400129 return status
130
131
132if __name__ == '__main__':
Anthony Sottile39ab2ed2021-10-23 13:23:50 -0400133 raise SystemExit(main())