diff --git a/cfbs/pretty.py b/cfbs/pretty.py index 588afeb6..def98f9e 100644 --- a/cfbs/pretty.py +++ b/cfbs/pretty.py @@ -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") @@ -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) diff --git a/tests/test_pretty.py b/tests/test_pretty.py index f3c5bd04..8ee23e42 100644 --- a/tests/test_pretty.py +++ b/tests/test_pretty.py @@ -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