Skip to content
208 changes: 120 additions & 88 deletions cfbs/pretty.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
from copy import copy
from collections import OrderedDict

MAX_LEN = 80
INDENT_SIZE = 2

# Globals for the keys in cfbs.json and their order
# Used for validation and prettifying / sorting.
TOP_LEVEL_KEYS = ("name", "description", "type", "index", "git", "provides", "build")
Expand Down Expand Up @@ -216,95 +219,124 @@ def pretty_string(s, sorting_rules=None):
return pretty(s, sorting_rules)


def pretty(o, sorting_rules=None):
MAX_LEN = 80
INDENT_SIZE = 2
def _should_wrap(parent, indent):
assert isinstance(parent, (tuple, list, dict))
# We should wrap the top level collection
if indent == 0:
return True
if isinstance(parent, dict):
parent = parent.values()

count = 0
for child in parent:
if isinstance(child, (tuple, list, dict)):
if len(child) >= 2:
count += 1
return count >= 2


def _encode_list_single_line(lst, indent, cursor):
buf = "["
last_index = len(lst) - 1
for index, child in enumerate(lst):
if index > 0:
buf += ", "
will_append_comma = index != last_index
buf += _encode(child, indent, cursor + len(buf), will_append_comma)
buf += "]"
return buf


def _encode_list_multiline(lst, indent):
indent += INDENT_SIZE
buf = "[\n" + " " * indent
last_index = len(lst) - 1
for index, child in enumerate(lst):
if index > 0:
buf += ",\n" + " " * indent
will_append_comma = index != last_index
buf += _encode(child, indent, 0, will_append_comma)
indent -= INDENT_SIZE
buf += "\n" + " " * indent + "]"
return buf


def _encode_list(lst, indent, cursor, will_append_comma):
if not lst:
return "[]"
if not _should_wrap(lst, indent):
buf = _encode_list_single_line(lst, indent, cursor)
adjust_for_comma = 1 if will_append_comma else 0
if (indent + cursor + len(buf)) <= (MAX_LEN - adjust_for_comma):
return buf
return _encode_list_multiline(lst, indent)


def _encode_dict_single_line(dct, indent, cursor):
buf = "{ "
last_index = len(dct) - 1
for index, (key, value) in enumerate(dct.items()):
if index > 0:
buf += ", "
if not isinstance(key, str):
raise ValueError("Illegal key type '" + type(key).__name__ + "'")
buf += '"' + key + '": '
will_append_comma = index != last_index
buf += _encode(value, indent, cursor + len(buf), will_append_comma)
buf += " }"
return buf


def _encode_dict_multiline(dct, indent):
indent += INDENT_SIZE
buf = "{\n" + " " * indent
last_index = len(dct) - 1
for index, (key, value) in enumerate(dct.items()):
if index > 0:
buf += ",\n" + " " * indent
if not isinstance(key, str):
raise ValueError("Illegal key type '" + type(key).__name__ + "'")
entry = '"' + key + '": '
will_append_comma = index != last_index
buf += entry + _encode(value, indent, len(entry), will_append_comma)
indent -= INDENT_SIZE
buf += "\n" + " " * indent + "}"
return buf


def _encode_dict(dct, indent, cursor, will_append_comma):
if not dct:
return "{}"
if not _should_wrap(dct, indent):
buf = _encode_dict_single_line(dct, indent, cursor)
adjust_for_comma = 1 if will_append_comma else 0
if (indent + cursor + len(buf)) <= (MAX_LEN - adjust_for_comma):
return buf
return _encode_dict_multiline(dct, indent)


def _encode(data, indent, cursor, will_append_comma):
if data is None:
return "null"
elif data is True:
return "true"
elif data is False:
return "false"
elif isinstance(data, (int, float)):
return repr(data)
elif isinstance(data, str):
# Use the json module to escape the string with backslashes:
return json.dumps(data)
elif isinstance(data, (list, tuple)):
return _encode_list(data, indent, cursor, will_append_comma)
elif isinstance(data, dict):
return _encode_dict(data, indent, cursor, will_append_comma)
else:
raise ValueError("Illegal value type '" + type(data).__name__ + "'")


def pretty(o, sorting_rules=None):
if sorting_rules is not None:
_children_sort(o, None, sorting_rules)

def _should_wrap(parent, indent):
assert isinstance(parent, (tuple, list, dict))
# We should wrap the top level collection
if indent == 0:
return True
if isinstance(parent, dict):
parent = parent.values()

count = 0
for child in parent:
if isinstance(child, (tuple, list, dict)):
if len(child) >= 2:
count += 1
return count >= 2

def _encode_list(lst, indent, cursor):
if not lst:
return "[]"
if not _should_wrap(lst, indent):
buf = json.dumps(lst)
assert "\n" not in buf
if indent + cursor + len(buf) <= MAX_LEN:
return buf

indent += INDENT_SIZE
buf = "[\n" + " " * indent
first = True
for value in lst:
if first:
first = False
else:
buf += ",\n" + " " * indent
buf += _encode(value, indent, 0)
indent -= INDENT_SIZE
buf += "\n" + " " * indent + "]"

return buf

def _encode_dict(dct, indent, cursor):
if not dct:
return "{}"
if not _should_wrap(dct, indent):
buf = json.dumps(dct)
buf = "{ " + buf[1 : len(buf) - 1] + " }"
assert "\n" not in buf
if indent + cursor + len(buf) <= MAX_LEN:
return buf

indent += INDENT_SIZE
buf = "{\n" + " " * indent
first = True
for key, value in dct.items():
if first:
first = False
else:
buf += ",\n" + " " * indent
if not isinstance(key, str):
raise ValueError("Illegal key type '" + type(key).__name__ + "'")
entry = '"' + key + '": '
buf += entry + _encode(value, indent, len(entry))
indent -= INDENT_SIZE
buf += "\n" + " " * indent + "}"

return buf

def _encode(data, indent, cursor):
if data is None:
return "null"
elif data is True:
return "true"
elif data is False:
return "false"
elif isinstance(data, (int, float)):
return repr(data)
elif isinstance(data, str):
# Use the json module to escape the string with backslashes:
return json.dumps(data)
elif isinstance(data, (list, tuple)):
return _encode_list(data, indent, cursor)
elif isinstance(data, dict):
return _encode_dict(data, indent, cursor)
else:
raise ValueError("Illegal value type '" + type(data).__name__ + "'")

return _encode(o, 0, 0)
return _encode(o, 0, 0, False)
83 changes: 83 additions & 0 deletions tests/test_pretty.py
Original file line number Diff line number Diff line change
Expand Up @@ -516,3 +516,86 @@ def test_pretty_sorting_real_examples():
}"""

assert pretty_string(test_json, cfbs_sorting_rules) == expected


def test_pretty_same_as_npm_prettier():
# We saw some cases where cfbs pretty and npm prettier did not agree
# Testing that this is no longer the case

test_json = """
{
"classes": { "My_class": {}, "My_class2": {"comment": "comment body"} }
}
"""

expected = """{
"classes": { "My_class": {}, "My_class2": { "comment": "comment body" } }
}"""

assert pretty_string(test_json) == expected

test_json = """
{
"filter": {
"filter": { "Attribute name": {"operator": "value2"} },
"hostFilter": {
"includes": {
"includeAdditionally": false,
"entries": {
"ip": ["192.168.56.5"],
"hostkey": [],
"hostname": ["ubuntu-bionic"],
"mac": ["08:00:27:0b:a4:99", "08:00:27:dd:e1:59", "02:9f:d3:59:7e:90"],
"ip_mask": ["10.0.2.16/16"]
}
},
"excludes": {
"entries": {
"ip": [],
"hostkey": [],
"hostname": [],
"mac": [],
"ip_mask": []
}
}
},
"hostContextExclude": ["class_value"],
"hostContextInclude": ["class_value"]
}
}
"""

expected = """{
"filter": {
"filter": { "Attribute name": { "operator": "value2" } },
"hostFilter": {
"includes": {
"includeAdditionally": false,
"entries": {
"ip": ["192.168.56.5"],
"hostkey": [],
"hostname": ["ubuntu-bionic"],
"mac": [
"08:00:27:0b:a4:99",
"08:00:27:dd:e1:59",
"02:9f:d3:59:7e:90"
],
"ip_mask": ["10.0.2.16/16"]
}
},
"excludes": {
"entries": {
"ip": [],
"hostkey": [],
"hostname": [],
"mac": [],
"ip_mask": []
}
}
},
"hostContextExclude": ["class_value"],
"hostContextInclude": ["class_value"]
}
}"""

assert pretty_string(test_json) == expected