Skip to content

Commit eeaa627

Browse files
committed
[pretty_format_json] Add compact array feature
- Find non-nested numeric arrays using JSON spec: https://www.json.org/json-en.html
1 parent 31903ea commit eeaa627

File tree

2 files changed

+150
-0
lines changed

2 files changed

+150
-0
lines changed

pre_commit_hooks/pretty_format_json.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import argparse
44
import json
5+
import re
56
import sys
67
from collections.abc import Mapping
78
from collections.abc import Sequence
@@ -14,6 +15,7 @@ def _get_pretty_format(
1415
ensure_ascii: bool = True,
1516
sort_keys: bool = True,
1617
top_keys: Sequence[str] = (),
18+
compact_arrays: bool = False,
1719
) -> str:
1820
def pairs_first(pairs: Sequence[tuple[str, str]]) -> Mapping[str, str]:
1921
before = [pair for pair in pairs if pair[0] in top_keys]
@@ -22,14 +24,58 @@ def pairs_first(pairs: Sequence[tuple[str, str]]) -> Mapping[str, str]:
2224
if sort_keys:
2325
after.sort()
2426
return dict(before + after)
27+
2528
json_pretty = json.dumps(
2629
json.loads(contents, object_pairs_hook=pairs_first),
2730
indent=indent,
2831
ensure_ascii=ensure_ascii,
2932
)
33+
34+
if compact_arrays:
35+
json_pretty = _compact_arrays(json_pretty)
36+
3037
return f'{json_pretty}\n'
3138

3239

40+
def _compact_arrays(json_text: str) -> str:
41+
"""Convert arrays with simple values to a single line format."""
42+
pattern = re.compile(
43+
r'''
44+
( # Capturing group for the entire array
45+
\[ # Opening bracket
46+
\s* # Optional whitespace
47+
(?: # Non-capturing group for array elements
48+
(?: # Non-capturing group for each value type
49+
"[^"]*" # String: anything in quotes
50+
|
51+
-? # Optional negative sign
52+
(?:
53+
0|[1-9]\d* # Integer part: 0 or non-zero digit
54+
# followed by digits
55+
)
56+
(?:\.\d+)? # Optional fractional part
57+
(?:[eE][+-]?\d+)? # Optional exponent part
58+
|
59+
true|false # Boolean
60+
|
61+
null # Null
62+
)
63+
(?:\s*,\s*)? # Optional comma and whitespace
64+
)++ # One or more elements
65+
\s* # Optional whitespace
66+
\] # Closing bracket
67+
)
68+
''', re.VERBOSE,
69+
)
70+
71+
def compact_match(match: re.Match[str]) -> str:
72+
array_content = match.group(0)
73+
compact = re.sub(r'\s*\n\s*', ' ', array_content)
74+
return compact
75+
76+
return re.sub(pattern, compact_match, json_text)
77+
78+
3379
def _autofix(filename: str, new_contents: str) -> None:
3480
print(f'Fixing file {filename}')
3581
with open(filename, 'w', encoding='UTF-8') as f:
@@ -96,6 +142,16 @@ def main(argv: Sequence[str] | None = None) -> int:
96142
default=[],
97143
help='Ordered list of keys to keep at the top of JSON hashes',
98144
)
145+
parser.add_argument(
146+
'--compact-arrays',
147+
action='store_true',
148+
dest='compact_arrays',
149+
default=False,
150+
help=(
151+
'Format simple arrays on a single line for more '
152+
'compact representation'
153+
),
154+
)
99155
parser.add_argument('filenames', nargs='*', help='Filenames to fix')
100156
args = parser.parse_args(argv)
101157

@@ -109,6 +165,7 @@ def main(argv: Sequence[str] | None = None) -> int:
109165
pretty_contents = _get_pretty_format(
110166
contents, args.indent, ensure_ascii=not args.no_ensure_ascii,
111167
sort_keys=not args.no_sort_keys, top_keys=args.top_keys,
168+
compact_arrays=args.compact_arrays,
112169
)
113170
except ValueError:
114171
print(

tests/pretty_format_json_test.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,96 @@ def test_diffing_output(capsys):
155155
assert actual_retval == expected_retval
156156
assert actual_out == expected_out
157157
assert actual_err == ''
158+
159+
160+
def test_compact_arrays_main(tmpdir):
161+
# TODO: Intentionally don't address round trip bug caused by
162+
# using `json.loads(json.dumps(data))`. This will need to be
163+
# resolved separately.
164+
srcfile = tmpdir.join('to_be_compacted.json')
165+
srcfile.write(
166+
'{\n'
167+
' "simple_array": [\n'
168+
' 1,\n'
169+
' 2,\n'
170+
' 3\n'
171+
' ],\n'
172+
' "string_array": [\n'
173+
' "a",\n'
174+
' "b",\n'
175+
' "c"\n'
176+
' ],\n'
177+
' "mixed_array": [\n'
178+
' 1,\n'
179+
' "string",\n'
180+
' true,\n'
181+
' null\n'
182+
' ],\n'
183+
' "nested_objects": [\n'
184+
' {\n'
185+
' "a": 1\n'
186+
' },\n'
187+
' {\n'
188+
' "b": 2\n'
189+
' }\n'
190+
' ]\n'
191+
'}',
192+
)
193+
194+
ret = main(['--compact-arrays', '--autofix', str(srcfile)])
195+
assert ret == 1
196+
197+
with open(str(srcfile), encoding='UTF-8') as f:
198+
contents = f.read()
199+
200+
# Simple arrays should be compacted
201+
assert '"simple_array": [ 1, 2, 3 ]' in contents
202+
assert '"string_array": [ "a", "b", "c" ]' in contents
203+
assert '"mixed_array": [ 1, "string", true, null ]' in contents
204+
205+
# Nested array objects should remain expanded
206+
assert ' "nested_objects": [\n' in contents
207+
assert ' "a": 1\n' in contents
208+
209+
210+
def test_compact_arrays_diff_output(tmpdir, capsys):
211+
srcfile = tmpdir.join('expanded_arrays.json')
212+
srcfile.write(
213+
'{\n'
214+
' "array": [\n'
215+
' 1,\n'
216+
' 2,\n'
217+
' 3\n'
218+
' ]\n'
219+
'}',
220+
)
221+
222+
ret = main(['--compact-arrays', str(srcfile)])
223+
assert ret == 1
224+
225+
out, _ = capsys.readouterr()
226+
assert '+ "array": [ 1, 2, 3 ]' in out
227+
228+
# Validate diff output
229+
assert '- 1,' in out
230+
assert '- 2,' in out
231+
assert '- 3' in out
232+
assert '- "array": [' in out
233+
assert '- ]' in out
234+
235+
236+
def test_compact_arrays_disabled(tmpdir):
237+
"""Test that compacting arrays does not impact default formatting."""
238+
srcfile = tmpdir.join('already_compact.json')
239+
srcfile.write('{\n "array": [ 1, 2, 3 ]\n}')
240+
241+
ret = main(['--autofix', str(srcfile)])
242+
assert ret == 1
243+
244+
with open(str(srcfile), encoding='UTF-8') as f:
245+
contents = f.read()
246+
247+
assert '"array": [\n' in contents
248+
assert ' 1,' in contents
249+
assert ' 2,' in contents
250+
assert ' 3\n ]' in contents

0 commit comments

Comments
 (0)