-
Notifications
You must be signed in to change notification settings - Fork 30
/
prettyjson.py
150 lines (124 loc) · 5.84 KB
/
prettyjson.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
def prettyjson(obj, indent=2, maxlinelength=80):
"""Renders JSON content with indentation and line splits/concatenations to fit maxlinelength.
Only dicts, lists and basic types are supported"""
items, _ = getsubitems(obj, itemkey="", islast=True,
maxlinelength=maxlinelength - indent, indent=indent)
return indentitems(items, indent, level=0)
def getsubitems(obj, itemkey, islast, maxlinelength, indent):
items = []
is_inline = True # at first, assume we can concatenate the inner tokens into one line
isdict = isinstance(obj, dict)
islist = isinstance(obj, list)
istuple = isinstance(obj, tuple)
isbasictype = not (isdict or islist or istuple)
maxlinelength = max(0, maxlinelength)
# build json content as a list of strings or child lists
if isbasictype:
# render basic type
keyseparator = "" if itemkey == "" else ": "
itemseparator = "" if islast else ","
items.append(itemkey + keyseparator +
basictype2str(obj) + itemseparator)
else:
# render lists/dicts/tuples
if isdict:
opening, closing, keys = ("{", "}", iter(obj.keys()))
elif islist or istuple:
opening, closing, keys = "[", "]", range(len(obj))
if itemkey != "":
opening = itemkey + ": " + opening
if not islast:
closing += ","
count = 0
itemkey = ""
subitems = []
# get the list of inner tokens
for (i, k) in enumerate(keys):
islast_ = i == len(obj)-1
itemkey_ = basictype2str(k) if isdict else ""
inner, is_inner_inline = getsubitems(
obj[k], itemkey_, islast_, maxlinelength - indent, indent)
# inner can be a string or a list
subitems.extend(inner)
# if a child couldn't be rendered inline, then we are not able either
is_inline = is_inline and is_inner_inline
# fit inner tokens into one or multiple lines, each no longer than maxlinelength
if is_inline:
multiline = True
# in Multi-line mode items of a list/dict/tuple can be rendered in multiple lines if they don't fit on one.
# suitable for large lists holding data that's not manually editable.
# in Single-line mode items are rendered inline if all fit in one line, otherwise each is rendered in a separate line.
# suitable for smaller lists or dicts where manual editing of individual items is preferred.
# this logic may need to be customized based on visualization requirements:
if (isdict):
multiline = False
if (islist):
multiline = True
if (multiline):
lines = []
current_line = ""
current_index = 0
for (i, item) in enumerate(subitems):
item_text = item
if i < len(inner)-1:
item_text = item + ","
if len(current_line) > 0:
try_inline = current_line + " " + item_text
else:
try_inline = item_text
if (len(try_inline) > maxlinelength):
# push the current line to the list if maxlinelength is reached
if len(current_line) > 0:
lines.append(current_line)
current_line = item_text
else:
# keep fitting all to one line if still below maxlinelength
current_line = try_inline
# Push the remainder of the content if end of list is reached
if (i == len(subitems)-1):
lines.append(current_line)
subitems = lines
if len(subitems) > 1:
is_inline = False
else: # single-line mode
totallength = len(subitems)-1 # spaces between items
for item in subitems:
totallength += len(item)
if (totallength <= maxlinelength):
str = "".join(item + " " for item in subitems)
# wrap concatenated content in a new list
subitems = [str.strip()]
else:
is_inline = False
# attempt to render the outer brackets + inner tokens in one line
if is_inline:
item_text = subitems[0] if subitems else ""
if len(opening) + len(item_text) + len(closing) <= maxlinelength:
items.append(opening + item_text + closing)
else:
is_inline = False
# if inner tokens are rendered in multiple lines already, then the outer brackets remain in separate lines
if not is_inline:
items.append(opening) # opening brackets
# Append children to parent list as a nested list
items.append(subitems)
items.append(closing) # closing brackets
return items, is_inline
def basictype2str(obj):
if isinstance(obj, str):
return "\"" + str(obj) + "\""
if isinstance(obj, bool):
return {True: "true", False: "false"}[obj]
return str(obj)
def indentitems(items, indent, level):
"""Recursively traverses the list of json lines, adds indentation based on the current depth"""
res = ""
indentstr = " " * (indent * level)
for (i, item) in enumerate(items):
if isinstance(item, list):
res += indentitems(item, indent, level+1)
else:
islast = (i == len(items)-1)
# no new line character after the last rendered line
res += indentstr + item if level == 0 and islast else indentstr + item + "\n"
return res