Repository: pre-commit/pre-commit-hooks
Pull request #408 (2019-08-15 16:42:33)

Add logic to print diff between expected and actual JSON
This PR makes the pretty JSON check more verbose when it encounters errors, that way developers can see which lines are causing errors in order to debug.

I haven't seen any talk about adding this anywhere, but would love to hear the maintainers' thoughts on this.


Patch:
diff --git a/pre_commit_hooks/pretty_format_json.py b/pre_commit_hooks/pretty_format_json.py
index e734ca8..cbc1b19 100644
--- a/pre_commit_hooks/pretty_format_json.py
+++ b/pre_commit_hooks/pretty_format_json.py
@@ -5,6 +5,7 @@ import io
 import json
 import sys
 from collections import OrderedDict
+from difflib import unified_diff
 from typing import List
 from typing import Mapping
 from typing import Optional
@@ -55,6 +56,13 @@ def parse_topkeys(s):  # type: (str) -> List[str]
     return s.split(',')
 
 
+def get_diff(source, target, file):  # type: (str, str, 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=None):  # type: (Optional[Sequence[str]]) -> int
     parser = argparse.ArgumentParser()
     parser.add_argument(
@@ -96,7 +104,6 @@ def main(argv=None):  # type: (Optional[Sequence[str]]) -> int
         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)
 
@@ -113,10 +120,19 @@ def main(argv=None):  # type: (Optional[Sequence[str]]) -> int
             )
 
             if contents != pretty_contents:
-                print('File {} is not pretty-formatted'.format(json_file))
+                print(
+                    'File {} is not pretty-formatted'.format(json_file),
+                    file=sys.stderr,
+                )
+                sys.stderr.flush()
 
                 if args.autofix:
                     _autofix(json_file, pretty_contents)
+                else:
+                    print(
+                        get_diff(contents, pretty_contents, json_file),
+                        end='',
+                    )
 
                 status = 1
         except ValueError:


Test patch:
diff --git a/tests/pretty_format_json_test.py b/tests/pretty_format_json_test.py
index 3b7b9a2..6cfe772 100644
--- a/tests/pretty_format_json_test.py
+++ b/tests/pretty_format_json_test.py
@@ -1,3 +1,4 @@
+import os
 import shutil
 
 import pytest
@@ -105,3 +106,36 @@ def test_top_sorted_get_pretty_format():
 def test_badfile_main():
     ret = main([get_resource_path('ok_yaml.yaml')])
     assert ret == 1
+
+
+def test_diffing_output(capsys):
+    resource_path = get_resource_path('not_pretty_formatted_json.json')
+    expected_retval = 1
+    a = os.path.join('a', resource_path)
+    b = os.path.join('b', resource_path)
+    expected_out = '''\
+--- {}
++++ {}
+@@ -1,6 +1,9 @@
+ {{
+-    "foo":
+-    "bar",
+-        "alist": [2, 34, 234],
+-  "blah": null
++  "alist": [
++    2,
++    34,
++    234
++  ],
++  "blah": null,
++  "foo": "bar"
+ }}
+'''.format(a, b)
+    expected_err = 'File {} is not pretty-formatted\n'.format(resource_path)
+
+    actual_retval = main([resource_path])
+    actual_out, actual_err = capsys.readouterr()
+
+    assert actual_retval == expected_retval
+    assert actual_out == expected_out
+    assert actual_err == expected_err