From f945e43f797ea3e6be505e23c93dca3257e6f5af Mon Sep 17 00:00:00 2001 From: killeroonie Date: Wed, 25 Jun 2025 16:15:09 -0700 Subject: [PATCH 1/4] Implemented guards against graph cycles and unit tests for same. Fixed typos and other linter problems. --- .idea/copyright/profiles_settings.xml | 7 + .../incubator/jsonpointer/pretty_printer.py | 133 +++-- .../jsonpointer/test_format_flags.py | 507 ----------------- .../jsonpointer/test_pretty_printer.py | 522 ++++++++++++++++++ .../jsonpointer/test_pretty_printer_cycles.py | 95 ++++ 5 files changed, 717 insertions(+), 547 deletions(-) create mode 100644 .idea/copyright/profiles_settings.xml delete mode 100644 tests/incubator/jsonpointer/test_format_flags.py create mode 100644 tests/incubator/jsonpointer/test_pretty_printer_cycles.py diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 0000000..82fea67 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/killerbunny/incubator/jsonpointer/pretty_printer.py b/killerbunny/incubator/jsonpointer/pretty_printer.py index a6be344..8cf33fd 100644 --- a/killerbunny/incubator/jsonpointer/pretty_printer.py +++ b/killerbunny/incubator/jsonpointer/pretty_printer.py @@ -1,15 +1,18 @@ +import logging from typing import NamedTuple from killerbunny.incubator.jsonpointer.constants import JSON_SCALARS, SCALAR_TYPES, JSON_VALUES, OPEN_BRACE, \ CLOSE_BRACE, \ SPACE, COMMA, EMPTY_STRING, CLOSE_BRACKET, OPEN_BRACKET +_logger = logging.getLogger(__name__) + # todo recursive code for printing list and dict members needs to detect cycles and have a maximum recursion depth class FormatFlags(NamedTuple): """Flags for various pretty printing options for Python nested JSON objects. - Standard defaults designed for debugging small nested dicts, and as_json_format() is useful for initializing - flags for printing in a json compatible format. + The default flags are designed for debugging small nested dicts, and as_json_format() is useful for initializing + flags for printing in a JSON-compatible format. The various "with_xxx()" methods make a copy of this instance's flags and allow you to set a specific flag. """ @@ -18,11 +21,14 @@ class FormatFlags(NamedTuple): use_repr: bool = False # when True format strings with str() instead of repr() format_json: bool = False # when True use "null" for "None" and "true" and "false" for True and False indent: int = 2 # number of spaces to indent each level of nesting - single_line: bool = True # when True format output as single line, when False over multiple lines - # when True do not insert commas after list and dict item elements - # note: when printing with single_line = True, if omit_commas is also True, output may be confusing - # as list and dict elements will have no obvious visual separation in the string, and parsing will be difficult - omit_commas: bool = False # when True do not insert commas after list and dict item elements + + # single_line: When True, format output as a single line, when False format as multiple lines + single_line: bool = True + + # omit_commas: When True do not insert commas after list and dict item elements + # note: when printing with single_line = True, if omit_commas is also True, output may be confusing since list and + # dict elements will have no obvious visual separation in the string, and parsing will be more complicated + omit_commas: bool = False # when True do not insert commas after `list` and `dict` item elements @staticmethod def as_json_format() ->"FormatFlags": @@ -71,10 +77,10 @@ def with_omit_commas(self, omit_commas: bool) -> "FormatFlags": def format_scalar(scalar_obj: JSON_SCALARS, format_: FormatFlags) -> str: """Format the scalar_obj according to the Format flags. - If the scalar_obj is None, returns None, or "null" if format_json is True - If the scalar_obj is a bool, returns True/False, or "true"/"false" if format_json is True - Otherwise, return str(scalar_obj), or repr(scalar_obj) if use_repr is True - If quote_strings is True, enclose str objects in quotes (single or double as specified by format_.single_quotes)) + If the scalar_obj is None, return None. Return "null" if format_.format_json is True + If the scalar_obj is a bool, return True/False, Return "true"/"false" if format_.format_json is True + Otherwise, return str(scalar_obj). Return repr(scalar_obj) if format_.use_repr is True + If quote_strings is True, enclose str objects in quotes (single or double as specified by format_.single_quotes) FormatFlags : format_.quote_strings: If True, enclose str objects in quotes, no quotes if False @@ -87,10 +93,10 @@ def format_scalar(scalar_obj: JSON_SCALARS, format_: FormatFlags) -> str: - :param scalar_obj: the scalar object to format + :param scalar_obj: The scalar object to format :param format_: Formatting flags used to specify formatting options - :return: the formatted object as a string, or 'None'/'null' if scalar_obj argument is None + :return: The formatted object as a str, or 'None'/'null' if the `scalar_obj` argument is None """ # no quotes used around JSON null, true, false literals if scalar_obj is None: @@ -111,7 +117,8 @@ def format_scalar(scalar_obj: JSON_SCALARS, format_: FormatFlags) -> str: # repr doesn't always escape a double quote in a str! # E.g.: repr() returns 'k"l' for "k"l", instead of "k\"l" which makes the JSON decoder fail. Frustrating! # todo investigate rules for valid JSON strings and issues with repr() - s = s.replace('"', '\\"') # todo do we need a regex for this to only replace " not preceeded by a \ ? + # todo do we need a regex for this to only replace " not preceded by a \ ? + s = s.replace('"', '\\"') else: s = str(scalar_obj) if isinstance(scalar_obj, str) and format_.quote_strings: @@ -125,8 +132,8 @@ def _spacer(format_: FormatFlags, level: int) -> str: return SPACE * ( format_.indent * level ) def _is_empty_or_single_item(obj: JSON_VALUES ) -> bool: - """Recurse the list or dict and return true if every nested element is either empty, - or contains exactly one scalar list element or one key/value pair where value is a single scalar value. + """Recurse the list or dict and return True if every nested element is either empty or contains + exactly one scalar list element or one key/value pair where the value is a single scalar value. Another way to think of this is, if the structure does not require a comma, this method will return True E.g. [ [ [ ] ] ] , [ [ [ "one" ] ] ] - both return True @@ -151,18 +158,38 @@ def _is_empty_or_single_item(obj: JSON_VALUES ) -> bool: else: return False -def _pp_dict(json_dict: dict[str, JSON_VALUES], format_: FormatFlags, lines: list[str], level: int = 0) -> list[str]: + +# noinspection DuplicatedCode +def _pp_dict(json_dict: dict[str, JSON_VALUES], + format_: FormatFlags, + lines: list[str], + level: int = 0, + instance_ids: dict[int, JSON_VALUES] | None = None, + ) -> list[str]: + if not isinstance(json_dict, dict): raise TypeError(f"Encountered non dict type: {type(json_dict)}") if len(lines) == 0: lines.append("") if lines[-1] != EMPTY_STRING: - indent_str = SPACE * ( format_.indent - 1) # current line already has text, so indent is relative to end of that text + # the current line already has text, so indent is relative to the end of that text + indent_str = SPACE * ( format_.indent - 1) elif len(lines) == 1 or level == 0: indent_str = EMPTY_STRING else: indent_str = _spacer(format_, level) + + if instance_ids is None: + instance_ids = {} # keeps track of instance ids to detect circular references + + if id(json_dict) in instance_ids: + # we have seen this list instance previously, cycle detected + _logger.warning(f"Cycle detected in json_dict: {json_dict}") + lines[-1] = f"{indent_str}{{...}}" + return lines + else: + instance_ids[id(json_dict)] = json_dict # save for future cycle detection if len(json_dict) == 0: lines[-1] += f"{indent_str}{OPEN_BRACE}{SPACE}{CLOSE_BRACE}" @@ -177,15 +204,16 @@ def _pp_dict(json_dict: dict[str, JSON_VALUES], format_: FormatFlags, lines: lis comma = EMPTY_STRING if format_.omit_commas else COMMA sp = SPACE if format_.single_line else EMPTY_STRING - lines[-1] += f"{indent_str}{OPEN_BRACE}" # start of dict text : { + lines[-1] += f"{indent_str}{OPEN_BRACE}" # start of the dict text: '{' level += 1 indent_str = _spacer(format_, level) for index, (key, value) in enumerate(json_dict.items()): # deal with commas + # noinspection PyUnusedLocal first_item: bool = (index == 0) - last_item: bool = (index == (len(json_dict) - 1 )) # no comma after last item + last_item: bool = (index == (len(json_dict) - 1 )) # no comma after the last item kf = format_scalar(key, format_) # formatted key if isinstance(value, SCALAR_TYPES): @@ -195,7 +223,7 @@ def _pp_dict(json_dict: dict[str, JSON_VALUES], format_: FormatFlags, lines: lis elif isinstance(value, list): lines.append("") lines[-1] = f"{indent_str}{kf}:" - # special case is where value is either an empty list or a list with one scalar element: + # special case is where the value is either an empty list or a list with one scalar element. # we can display this value on the same line as the key name. if len(value) > 1: lines.append("") @@ -206,11 +234,11 @@ def _pp_dict(json_dict: dict[str, JSON_VALUES], format_: FormatFlags, lines: lis ... else: lines.append("") - _pp_list(value, format_, lines, level) + _pp_list(value, format_, lines, level, instance_ids) elif isinstance(value, dict): lines.append("") lines[-1] = f"{indent_str}{kf}:" - # special case is where value is either an empty dict or a dict with one key with a scalar value: + # special case is where the value is either an empty dict or a dict with one key with a scalar value: # we can display the nested dict on the same line as the key name of the parent dict. if len(value) > 1: lines.append("") @@ -218,7 +246,7 @@ def _pp_dict(json_dict: dict[str, JSON_VALUES], format_: FormatFlags, lines: lis nk, nv = next(iter(value.items())) if not isinstance(nv, SCALAR_TYPES): lines.append("") - _pp_dict(value, format_, lines, level) + _pp_dict(value, format_, lines, level, instance_ids) if not last_item: lines[-1] += comma @@ -235,8 +263,13 @@ def _pp_dict(json_dict: dict[str, JSON_VALUES], format_: FormatFlags, lines: lis return lines - -def _pp_list(json_list: list[JSON_VALUES], format_: FormatFlags, lines: list[str], level: int = 0) -> list[str]: +# noinspection DuplicatedCode +def _pp_list(json_list: list[JSON_VALUES], + format_: FormatFlags, + lines: list[str], + level: int = 0, + instance_ids: dict[int, JSON_VALUES] | None = None, + ) -> list[str]: if not isinstance(json_list, list): raise TypeError(f"Encountered non list type: {type(json_list)}") @@ -245,11 +278,24 @@ def _pp_list(json_list: list[JSON_VALUES], format_: FormatFlags, lines: list[str lines.append("") if lines[-1] != EMPTY_STRING: - indent_str = SPACE * ( format_.indent - 1) # current line already has text, so indent is relative to end of that text + # the current line already has text, so indent is relative to the end of that text + indent_str = SPACE * ( format_.indent - 1) elif len(lines) == 1 or level == 0: indent_str = EMPTY_STRING else: indent_str = _spacer(format_, level) + + if instance_ids is None: + instance_ids = {} # keeps track of instance ids to detect circular references + + if id(json_list) in instance_ids: + # we have seen this list instance previously, cycle detected + _logger.warning(f"Cycle detected in json_list: {json_list}") + lines[-1] = f"{indent_str}[...]" + return lines + else: + instance_ids[id(json_list)] = json_list # save for future cycle detection + if len(json_list) == 0: lines[-1] += f"{indent_str}{OPEN_BRACKET}{SPACE}{CLOSE_BRACKET}" @@ -268,20 +314,20 @@ def _pp_list(json_list: list[JSON_VALUES], format_: FormatFlags, lines: list[str for index, item in enumerate(json_list): first_item: bool = (index == 0) - last_item: bool = (index == (len(json_list) - 1 )) # no comma after last element + last_item: bool = (index == (len(json_list) - 1 )) # no comma after the last element if isinstance(item, SCALAR_TYPES): lines.append("") s = format_scalar(item, format_) lines[-1] = f"{indent_str}{s}" elif isinstance(item, list): - if not first_item: # if this is a new list starting inside of list, open brackets can go on same line + if not first_item: # if this is a new list starting inside the list, open brackets can go on the same line lines.append("") - _pp_list(item, format_, lines, level) + _pp_list(item, format_, lines, level, instance_ids) elif isinstance(item, dict): - if not first_item: # if this is a new dict starting inside of list, open brackets can go on same line + if not first_item: # if this is a new dict starting inside the list, open brackets can go on the same line lines.append("") - _pp_dict(item, format_, lines, level) + _pp_dict(item, format_, lines, level, instance_ids) if not last_item: lines[-1] += comma @@ -296,23 +342,30 @@ def _pp_list(json_list: list[JSON_VALUES], format_: FormatFlags, lines: list[str return lines -def pretty_print(json_obj: JSON_VALUES, format_: FormatFlags, lines: list[str], indent_level: int = 0) -> str: +def pretty_print(json_obj: JSON_VALUES, + format_: FormatFlags, + lines: list[str] | None = None, + indent_level: int = 0, + ) -> str: """Return the JSON value formatted as a str according to the flags in the format_ argument. - Typically, an empty list is passed to this method. Each generated line of formatted outut is appended - to the lines list argument. - When this method returns, the lines argument will contain each line in the formatted str, or a single new + Typically, an empty list is passed to this method. Each generated line of formatted output is appended + to the `lines` list argument. + When this method returns, the `lines` argument will contain each line in the formatted str, or a single new element if format_.single_line is True. These lines are then joined() and returned. """ - - lines.append("") # so format methods will have a new starting line for output + if lines is None or len(lines) == 0: + lines = [""] # so format methods will have a new starting line for output + + instance_ids: dict[int, JSON_VALUES] = {} # keeps track of instance ids to detect circular references + if isinstance(json_obj, SCALAR_TYPES): lines[-1] = format_scalar(json_obj, format_) elif isinstance(json_obj, list): - _pp_list(json_obj, format_, lines, indent_level) + _pp_list(json_obj, format_, lines, indent_level, instance_ids) elif isinstance(json_obj, dict): - _pp_dict(json_obj, format_, lines, indent_level) + _pp_dict(json_obj, format_, lines, indent_level, instance_ids) else: raise ValueError(f"Unsupported type: {type(json_obj)}") diff --git a/tests/incubator/jsonpointer/test_format_flags.py b/tests/incubator/jsonpointer/test_format_flags.py deleted file mode 100644 index 3bb4890..0000000 --- a/tests/incubator/jsonpointer/test_format_flags.py +++ /dev/null @@ -1,507 +0,0 @@ - -import unittest - -from killerbunny.incubator.jsonpointer.constants import JSON_VALUES -from killerbunny.incubator.jsonpointer.pretty_printer import FormatFlags, format_scalar, _spacer, \ - _is_empty_or_single_item, _pp_list, \ - _pp_dict, pretty_print - -# unittest uses (expected, actual) in asserts whereas pytest uses (actual, expected) to my eternal confusion - -class TestFormatFlags(unittest.TestCase): - def test_as_json_format(self) -> None: - flags = FormatFlags.as_json_format() - self.assertTrue(flags.quote_strings) - self.assertFalse(flags.single_quotes) - self.assertTrue(flags.use_repr) - self.assertTrue(flags.format_json) - self.assertEqual(flags.indent, 2) - self.assertFalse(flags.single_line) - self.assertFalse(flags.omit_commas) - - def test_with_indent(self) -> None: - flags = FormatFlags().with_indent(4) - self.assertEqual(flags.indent, 4) - self.assertFalse(flags.quote_strings) - - def test_with_quote_strings(self) -> None: - flags = FormatFlags().with_quote_strings(True) - self.assertTrue(flags.quote_strings) - self.assertFalse(flags.single_quotes) - - def test_with_single_quotes(self) -> None: - flags = FormatFlags().with_single_quotes(True) - self.assertTrue(flags.single_quotes) - self.assertFalse(flags.quote_strings) - - def test_with_use_repr(self) -> None: - flags = FormatFlags().with_use_repr(True) - self.assertTrue(flags.use_repr) - - def test_with_format_json(self) -> None: - flags = FormatFlags().with_format_json(True) - self.assertTrue(flags.format_json) - - def test_with_single_line(self) -> None: - flags = FormatFlags().with_single_line(True) - self.assertTrue(flags.single_line) - self.assertFalse(flags.omit_commas) - flags = FormatFlags().with_single_line(False) - self.assertFalse(flags.single_line) - self.assertFalse(flags.omit_commas) - flags = FormatFlags().with_single_line(False).with_omit_commas(True) - self.assertTrue(flags.omit_commas) - - def test_with_omit_commas(self) -> None: - flags = FormatFlags().with_omit_commas(True) - self.assertTrue(flags.omit_commas) - - -class TestPPScalar(unittest.TestCase): - def test_none(self) -> None: - self.assertEqual(format_scalar(None, FormatFlags()), "None") - self.assertEqual(format_scalar(None, FormatFlags().with_format_json(True)), "null") - - def test_bool(self) -> None: - self.assertEqual(format_scalar(True, FormatFlags()), "True") - self.assertEqual(format_scalar(False, FormatFlags()), "False") - self.assertEqual(format_scalar(True, FormatFlags().with_format_json(True)), "true") - self.assertEqual(format_scalar(False, FormatFlags().with_format_json(True)), "false") - - def test_string(self) -> None: - self.assertEqual(format_scalar("hello", FormatFlags()), "hello") - self.assertEqual(format_scalar("hello", FormatFlags().with_quote_strings(True)), '"hello"') - self.assertEqual(format_scalar("hello", FormatFlags().with_quote_strings(True).with_single_quotes(True)), "'hello'") - self.assertEqual(format_scalar("hello", FormatFlags().with_use_repr(True)), "hello") - self.assertEqual(format_scalar("hello", FormatFlags().with_use_repr(True).with_quote_strings(True)), '"hello"') - self.assertEqual(format_scalar("k\"l", FormatFlags().with_use_repr(True).with_quote_strings(True)), '"k\\"l"') - - def test_number(self) -> None: - self.assertEqual(format_scalar(123, FormatFlags()), "123") - self.assertEqual(format_scalar(3.14, FormatFlags()), "3.14") - self.assertEqual(format_scalar(123, FormatFlags().with_use_repr(True)), "123") - self.assertEqual(format_scalar(3.14, FormatFlags().with_use_repr(True)), "3.14") - - -class TestSpacer(unittest.TestCase): - def test_single_line(self) -> None: - self.assertEqual(" ", _spacer(FormatFlags().with_single_line(True), 2)) - - def test_multi_line(self) -> None: - self.assertEqual(_spacer(FormatFlags().with_single_line(False), 2), " ") - self.assertEqual(_spacer(FormatFlags().with_single_line(False).with_indent(4), 3), " ") - - -class TestIsEmptyOrSingleItem(unittest.TestCase): - def test_scalar(self) -> None: - self.assertTrue(_is_empty_or_single_item(1)) - self.assertTrue(_is_empty_or_single_item("hello")) - - def test_empty_list(self) -> None: - self.assertTrue(_is_empty_or_single_item([])) - - def test_single_item_list(self) -> None: - self.assertTrue(_is_empty_or_single_item([1])) - self.assertTrue(_is_empty_or_single_item(["hello"])) - self.assertTrue(_is_empty_or_single_item([[]])) - self.assertTrue(_is_empty_or_single_item([[[[]]]])) - self.assertTrue(_is_empty_or_single_item([[[["one"]]]])) - self.assertTrue(_is_empty_or_single_item([{"key": "one"}])) - self.assertTrue(_is_empty_or_single_item([{"key": [["one"]]}])) - self.assertTrue(_is_empty_or_single_item([[{"key": [["foo"]]}]])) - self.assertTrue(_is_empty_or_single_item([{"key1": {"key2": {"key3": "foo"}}}])) - - def test_multi_item_list(self) -> None: - self.assertFalse(_is_empty_or_single_item([1, 2])) - self.assertFalse(_is_empty_or_single_item(["hello", "world"])) - self.assertFalse(_is_empty_or_single_item([["one", "two"]])) - self.assertFalse(_is_empty_or_single_item([[[["one", "two"]]]])) - self.assertFalse(_is_empty_or_single_item([{"key": [["one", "two"]]}])) - self.assertFalse(_is_empty_or_single_item([[{"one": "foo", "two": "bar"}]])) - - def test_empty_dict(self) -> None: - self.assertTrue(_is_empty_or_single_item({})) - - def test_single_item_dict(self) -> None: - self.assertTrue(_is_empty_or_single_item({"key": 1})) - self.assertTrue(_is_empty_or_single_item({"key": "hello"})) - self.assertTrue(_is_empty_or_single_item({"key": []})) - self.assertTrue(_is_empty_or_single_item({"key": [1]})) - self.assertTrue(_is_empty_or_single_item({"key": [["one"]]})) - self.assertTrue(_is_empty_or_single_item({"key": {}})) - self.assertTrue(_is_empty_or_single_item({"key": {"subkey": "value"}})) - self.assertTrue(_is_empty_or_single_item({"key1": {"key2": {"key3": "foo"}}})) - - def test_multi_item_dict(self) -> None: - self.assertFalse(_is_empty_or_single_item({"key1": 1, "key2": 2})) - self.assertFalse(_is_empty_or_single_item({"key": [1, 2]})) - self.assertFalse(_is_empty_or_single_item({"key": {"subkey1": "value1", "subkey2": "value2"}})) - self.assertFalse(_is_empty_or_single_item({"key": [[{"one": "foo", "two": "bar"}]]})) - - -class TestPPDictNew(unittest.TestCase): - def test_empty_dict(self) -> None: - lines = [""] - _pp_dict({}, FormatFlags(), lines) - self.assertEqual(lines, ["{ }"]) - - def test_single_scalar_dict(self) -> None: - lines = [""] - _pp_dict({"key": "value"}, FormatFlags(), lines) - self.assertEqual(lines, ["{ key: value }"]) - - def test_multi_scalar_dict(self) -> None: - lines = [""] - _pp_dict({"key1": "value1", "key2": "value2"}, FormatFlags(), lines) - self.assertEqual(["{", " key1: value1,", " key2: value2", " }"],lines) - - def test_nested_list_dict(self) -> None: - lines = [""] - _pp_dict({"key": ["item1", "item2"]}, FormatFlags(), lines) - self.assertEqual(['{', ' key:', ' [', ' item1,', ' item2', ' ]', ' }'], lines) - - def test_nested_dict_dict(self) -> None: - lines = [""] - _pp_dict({"key": {"subkey": "subvalue"}}, FormatFlags().with_single_line(False), lines) - self.assertEqual(['{', ' key: { subkey: subvalue } }'], lines) - - def test_complex_dict(self) -> None: - data: dict[str, JSON_VALUES] = { - "key1": "value1", - "key2": ["item1", "item2"], - "key3": {"subkey1": "subvalue1", "subkey2": "subvalue2"}, - "key4": 123 - } - lines = [""] - _pp_dict(data, FormatFlags().with_single_line(False), lines) - self.assertEqual(["{", " key1: value1,", " key2:", " [", " item1,", " item2", " ],", " key3:", - " {", " subkey1: subvalue1,", " subkey2: subvalue2", " },", " key4: 123", "}"], lines) - - def test_single_line_dict(self) -> None: - lines = [""] - _pp_dict({"key1": "value1", "key2": "value2"}, FormatFlags().with_single_line(True), lines) - self.assertEqual(['{', ' key1: value1,', ' key2: value2', ' }'], lines) - - def test_single_item_list_in_dict(self) -> None: - lines = [""] - _pp_dict({"key": ["item1"]}, FormatFlags(), lines) - self.assertEqual(['{', ' key: [ item1 ] }'], lines) - - def test_single_item_dict_in_dict(self) -> None: - lines = [""] - _pp_dict({"key": {"subkey": "subvalue"}}, FormatFlags(), lines) - self.assertEqual(['{', ' key: { subkey: subvalue } }'], lines) - - def test_single_item_nested_list_in_dict(self) -> None: - lines = [""] - _pp_dict({"key": [["item1"]]}, FormatFlags(), lines) - self.assertEqual(['{', ' key: [ [ item1 ] ] }'], lines) - - def test_single_item_nested_dict_in_dict(self) -> None: - lines = [""] - _pp_dict({"key": [{"subkey": "subvalue"}]}, FormatFlags(), lines) - self.assertEqual(['{', ' key: [ { subkey: subvalue } ] }'], lines) - - def test_single_item_nested_dict_in_dict_2(self) -> None: - lines = [""] - _pp_dict({"key": {"subkey": ["subvalue"]}}, FormatFlags(), lines) - self.assertEqual(['{', ' key:', ' {', ' subkey: [ subvalue ] } }'], lines) - - def test_single_item_nested_dict_in_dict_3(self) -> None: - lines = [""] - _pp_dict({"key": {"subkey": {"subsubkey": "subvalue"}}}, FormatFlags(), lines) - self.assertEqual(['{', ' key:', ' {', ' subkey: { subsubkey: subvalue } } }'], lines) - - def test_single_item_nested_dict_in_dict_4(self) -> None: - lines = [""] - _pp_dict({"key": {"subkey": {"subsubkey": ["subvalue"]}}}, FormatFlags(), lines) - self.assertEqual(['{', ' key:', ' {', ' subkey:', ' {', ' subsubkey: [ subvalue ] } } }'], lines) - - def test_single_item_nested_dict_in_dict_5(self) -> None: - lines = [""] - _pp_dict({"key": {"subkey": {"subsubkey": [{"subsubsubkey": "subvalue"}]}}}, FormatFlags().with_single_line(False), lines) - expected = ['{', - ' key:', - ' {', - ' subkey:', - ' {', - ' subsubkey: [ { subsubsubkey: subvalue } ] } } }'] - self.assertEqual(expected, lines) - - def test_single_item_nested_dict_in_dict_6(self) -> None: - lines = [""] - _pp_dict({"key": {"subkey": {"subsubkey": [{"subsubsubkey": ["subvalue"]}]}}}, FormatFlags(), lines) - expected = ['{', - ' key:', - ' {', - ' subkey:', - ' {', - ' subsubkey: [ {', - ' subsubsubkey: [ subvalue ] } ] } } }'] - self.assertEqual(expected, lines) - - def test_single_item_nested_dict_in_dict_7(self) -> None: - lines = [""] - _pp_dict({"key": {"subkey": {"subsubkey": [{"subsubsubkey": [{"subsubsubsubkey": "subvalue"}]}]}}}, - FormatFlags(), lines) - expected = ['{', - ' key:', - ' {', - ' subkey:', - ' {', - ' subsubkey: [ {', - ' subsubsubkey: [ { subsubsubsubkey: subvalue } ] } ] } } }'] - self.assertEqual(expected, lines) - - def test_single_item_nested_dict_in_dict_8(self) -> None: - lines = [""] - _pp_dict({"key": {"subkey": {"subsubkey": [{"subsubsubkey": [{"subsubsubsubkey": ["subvalue"]}]}]}}}, - FormatFlags().with_single_line(False), lines) - expected = ['{', - ' key:', - ' {', - ' subkey:', - ' {', - ' subsubkey: [ {', - ' subsubsubkey: [ {', - ' subsubsubsubkey: [ subvalue ] } ] } ] } } }'] - self.assertEqual(expected, lines) - - def test_single_item_nested_dict_in_dict_9(self) -> None: - lines = [""] - _pp_dict({"key": { - "subkey": {"subsubkey": [{"subsubsubkey": [{"subsubsubsubkey": [{"subsubsubsubsubkey": "subvalue"}]}]}]}}}, - FormatFlags().with_single_line(False), lines) - expected = ['{', - ' key:', - ' {', - ' subkey:', - ' {', - ' subsubkey: [ {', - ' subsubsubkey: [ {', - ' subsubsubsubkey: [ { subsubsubsubsubkey: subvalue } ] } ] } ] ' - '} } }'] - self.assertEqual(expected, lines) - - def test_single_item_nested_dict_in_dict_10(self) -> None: - lines = [""] - _pp_dict({"key": {"subkey": { - "subsubkey": [{"subsubsubkey": [{"subsubsubsubkey": [{"subsubsubsubsubkey": ["subvalue"]}]}]}]}}}, - FormatFlags().with_single_line(False), lines) - expected = ['{', - ' key:', - ' {', - ' subkey:', - ' {', - ' subsubkey: [ {', - ' subsubsubkey: [ {', - ' subsubsubsubkey: [ {', - ' subsubsubsubsubkey: [ subvalue ] } ] } ] } ] } } }'] - self.assertEqual(expected, lines) - -class TestPPList(unittest.TestCase): - def test_empty_list(self) -> None: - lines = [""] - _pp_list([], FormatFlags(), lines) - expected = ["[ ]"] - self.assertEqual(expected,lines ) - - def test_single_scalar_list(self) -> None: - lines = [""] - _pp_list([1], FormatFlags(), lines) - expected = ["[ 1 ]"] - self.assertEqual(expected, lines) - - def test_multi_scalar_list(self) -> None: - lines = [""] - _pp_list([1, 2, 3], FormatFlags().with_single_line(True), lines) - expected = ['[', ' 1,', ' 2,', ' 3', ' ]'] - self.assertEqual(expected, lines) - - def test_nested_list(self) -> None: - lines = [""] - _pp_list([[1, 2], [3, 4]], FormatFlags(), lines) - expected = ['[ [', ' 1,', ' 2', ' ],', ' [', ' 3,', ' 4', ' ]', ' ]'] - self.assertEqual(expected, lines) - - def test_nested_dict_list(self) -> None: - lines = [""] - _pp_list([{"key1": "value1"}, {"key2": "value2"}], FormatFlags(), lines) - expected = ['[ { key1: value1 },', ' { key2: value2 }', ' ]'] - self.assertEqual(expected, lines) - - def test_complex_list(self) -> None: - data: list[JSON_VALUES] = [ - 1, - [2, 3], - {"key1": "value1", "key2": "value2"}, - 4 - ] - lines = [""] - _pp_list(data, FormatFlags().with_single_line(False), lines) - expected = ["[", " 1,", " [", " 2,", " 3", " ],", " {", " key1: value1,", " key2: value2", - " },", " 4", "]"] - self.assertEqual( expected, lines) - - def test_single_line_list(self) -> None: - lines = [""] - _pp_list([1, 2, 3], FormatFlags().with_single_line(True), lines) - expected = ['[', ' 1,', ' 2,', ' 3', ' ]'] - self.assertEqual(expected,lines ) - - def test_single_item_list_in_list(self) -> None: - lines = [""] - _pp_list([[1]], FormatFlags(), lines) - expected = ['[ [ 1 ] ]'] - self.assertEqual(expected, lines ) - - def test_single_item_dict_in_list(self) -> None: - lines = [""] - _pp_list([{"key": "value"}], FormatFlags(), lines) - expected =['[ { key: value } ]'] - self.assertEqual(expected, lines ) - - def test_single_item_nested_list_in_list(self) -> None: - lines = [""] - _pp_list([[[1]]], FormatFlags(), lines) - expected = ['[ [ [ 1 ] ] ]'] - self.assertEqual(expected, lines ) - - def test_single_item_nested_dict_in_list(self) -> None: - lines = [""] - _pp_list([[{"key": "value"}]], FormatFlags(), lines) - expected = ['[ [ { key: value } ] ]'] - self.assertEqual(expected, lines ) - - def test_single_item_nested_dict_in_list_2(self) -> None: - lines = [""] - _pp_list([{"key": [1]}], FormatFlags(), lines) - expected = ['[ {', ' key: [ 1 ] } ]'] - self.assertEqual(expected, lines ) - - def test_single_item_nested_dict_in_list_3(self) -> None: - lines = [""] - _pp_list([{"key": {"subkey": "value"}}], FormatFlags(), lines) - expected = ['[ {', ' key: { subkey: value } } ]'] - self.assertEqual(expected, lines ) - - def test_single_item_nested_dict_in_list_4(self) -> None: - lines = [""] - _pp_list([{"key": {"subkey": [1]}}], FormatFlags(), lines) - expected = ['[ {', ' key:', ' {', ' subkey: [ 1 ] } } ]'] - self.assertEqual(expected, lines ) - - def test_single_item_nested_dict_in_list_5(self) -> None: - lines = [""] - _pp_list([{"key": {"subkey": [{"subsubkey": "value"}]}}], FormatFlags(), lines) - expected = ['[ {', ' key:', ' {', ' subkey: [ { subsubkey: value } ] } } ]'] - self.assertEqual(expected, lines ) - - def test_single_item_nested_dict_in_list_6(self) -> None: - lines = [""] - _pp_list([{"key": {"subkey": [{"subsubkey": [1]}]}}], FormatFlags(), lines) - expected = ['[ {', ' key:', ' {', ' subkey: [ {', ' subsubkey: [ 1 ] } ] } } ]'] - self.assertEqual(expected, lines ) - - def test_single_item_nested_dict_in_list_7(self) -> None: - lines = [""] - _pp_list([{"key": {"subkey": [{"subsubkey": [{"subsubsubkey": "value"}]}]}}], FormatFlags(), lines) - expected = ['[ {', - ' key:', - ' {', - ' subkey: [ {', - ' subsubkey: [ { subsubsubkey: value } ] } ] } } ]'] - self.assertEqual(expected, lines ) - - def test_single_item_nested_dict_in_list_8(self) -> None: - lines = [""] - _pp_list([{"key": {"subkey": [{"subsubkey": [{"subsubsubkey": [1]}]}]}}], FormatFlags(), lines) - expected =['[ {', - ' key:', - ' {', - ' subkey: [ {', - ' subsubkey: [ {', - ' subsubsubkey: [ 1 ] } ] } ] } } ]'] - self.assertEqual(expected, lines ) - - def test_single_item_nested_dict_in_list_9(self) -> None: - lines = [""] - _pp_list([{"key": {"subkey": [{"subsubkey": [{"subsubsubkey": [{"subsubsubsubkey": "value"}]}]}]}}], - FormatFlags(), lines) - expected =['[ {', - ' key:', - ' {', - ' subkey: [ {', - ' subsubkey: [ {', - ' subsubsubkey: [ { subsubsubsubkey: value } ] } ] } ] } } ]'] - self.assertEqual(expected, lines ) - - def test_single_item_nested_dict_in_list_10(self) -> None: - lines = [""] - _pp_list([{"key": {"subkey": [{"subsubkey": [{"subsubsubkey": [{"subsubsubsubkey": [1]}]}]}]}}], FormatFlags(), - lines) - expected = ['[ {', - ' key:', - ' {', - ' subkey: [ {', - ' subsubkey: [ {', - ' subsubsubkey: [ {', - ' subsubsubsubkey: [ 1 ] } ] } ] } ] } } ]'] - self.assertEqual(expected, lines) - - -class TestPrettyPrint2(unittest.TestCase): - def test_scalar(self) -> None: - self.assertEqual(pretty_print(1, FormatFlags(), []), "1") - self.assertEqual(pretty_print("hello", FormatFlags(), []), "hello") - - def test_list(self) -> None: - self.assertEqual(pretty_print([1, 2, 3], FormatFlags().with_single_line(False), []), "[\n 1,\n 2,\n 3\n]") - self.assertEqual(pretty_print([1, 2, 3], FormatFlags().with_single_line(True), []), "[ 1, 2, 3 ]") - - def test_dict(self) -> None: - self.assertEqual(pretty_print({"key1": "value1", "key2": "value2"}, FormatFlags().with_single_line(False), []), - "{\n key1: value1,\n key2: value2\n}") - self.assertEqual(pretty_print({"key1": "value1", "key2": "value2"}, FormatFlags().with_single_line(True), []), - "{ key1: value1, key2: value2 }") - - def test_complex(self) -> None: - data: dict[str, JSON_VALUES] = { - "key1": "value1", - "key2": [1, 2, 3], - "key3": {"subkey1": "subvalue1", "subkey2": "subvalue2"} - } - expected = "{\n key1: value1,\n key2:\n [\n 1,\n 2,\n 3\n ],\n key3:\n {\n subkey1: subvalue1,\n subkey2: subvalue2\n }\n}" - self.assertEqual(pretty_print(data, FormatFlags().with_single_line(False), []), expected) - - def test_complex_single_line(self) -> None: - data: dict[str, JSON_VALUES] = { - "key1": "value1", - "key2": [1, 2, 3], - "key3": {"subkey1": "subvalue1", "subkey2": "subvalue2"} - } - expected = "{ key1: value1, key2: [ 1, 2, 3 ], key3: { subkey1: subvalue1, subkey2: subvalue2 } }" - self.assertEqual(pretty_print(data, FormatFlags().with_single_line(True), []), expected) - - def test_empty_list(self) -> None: - self.assertEqual(pretty_print([], FormatFlags(), []), "[ ]") - - def test_empty_dict(self) -> None: - self.assertEqual(pretty_print({}, FormatFlags(), []), "{ }") - - def test_nested_empty_list(self) -> None: - self.assertEqual(pretty_print([[]], FormatFlags().with_single_line(False), []), "[ [ ] ]") - self.assertEqual(pretty_print([[]], FormatFlags().with_single_line(True), []), "[ [ ] ]") - - - def test_nested_empty_dict(self) -> None: - self.assertEqual(pretty_print([{}], FormatFlags().with_single_line(False), []), "[ { } ]") - self.assertEqual(pretty_print([{}], FormatFlags().with_single_line(True), []), "[ { } ]") - - - def test_nested_empty_list_single_line(self) -> None: - self.assertEqual(pretty_print([[]], FormatFlags().with_single_line(True), []), "[ [ ] ]") - - def test_nested_empty_dict_single_line(self) -> None: - self.assertEqual(pretty_print([{}], FormatFlags().with_single_line(True), []), "[ { } ]") - diff --git a/tests/incubator/jsonpointer/test_pretty_printer.py b/tests/incubator/jsonpointer/test_pretty_printer.py index b28b04f..c7ee84e 100644 --- a/tests/incubator/jsonpointer/test_pretty_printer.py +++ b/tests/incubator/jsonpointer/test_pretty_printer.py @@ -1,3 +1,525 @@ +import unittest +from killerbunny.incubator.jsonpointer.constants import JSON_VALUES +# noinspection PyProtectedMember +from killerbunny.incubator.jsonpointer.pretty_printer import FormatFlags, format_scalar, _spacer, \ + _is_empty_or_single_item, _pp_list, \ + _pp_dict, pretty_print + +# unittest uses (expected, actual) in asserts whereas pytest uses (actual, expected) to my eternal confusion + +class TestFormatFlags(unittest.TestCase): + def test_as_json_format(self) -> None: + flags = FormatFlags.as_json_format() + self.assertTrue(flags.quote_strings) + self.assertFalse(flags.single_quotes) + self.assertTrue(flags.use_repr) + self.assertTrue(flags.format_json) + self.assertEqual(flags.indent, 2) + self.assertFalse(flags.single_line) + self.assertFalse(flags.omit_commas) + + def test_with_indent(self) -> None: + flags = FormatFlags().with_indent(4) + self.assertEqual(flags.indent, 4) + self.assertFalse(flags.quote_strings) + + def test_with_quote_strings(self) -> None: + flags = FormatFlags().with_quote_strings(True) + self.assertTrue(flags.quote_strings) + self.assertFalse(flags.single_quotes) + + def test_with_single_quotes(self) -> None: + flags = FormatFlags().with_single_quotes(True) + self.assertTrue(flags.single_quotes) + self.assertFalse(flags.quote_strings) + + def test_with_use_repr(self) -> None: + flags = FormatFlags().with_use_repr(True) + self.assertTrue(flags.use_repr) + + def test_with_format_json(self) -> None: + flags = FormatFlags().with_format_json(True) + self.assertTrue(flags.format_json) + + def test_with_single_line(self) -> None: + flags = FormatFlags().with_single_line(True) + self.assertTrue(flags.single_line) + self.assertFalse(flags.omit_commas) + flags = FormatFlags().with_single_line(False) + self.assertFalse(flags.single_line) + self.assertFalse(flags.omit_commas) + flags = FormatFlags().with_single_line(False).with_omit_commas(True) + self.assertTrue(flags.omit_commas) + + def test_with_omit_commas(self) -> None: + flags = FormatFlags().with_omit_commas(True) + self.assertTrue(flags.omit_commas) + + +class TestPPScalar(unittest.TestCase): + def test_none(self) -> None: + self.assertEqual(format_scalar(None, FormatFlags()), "None") + self.assertEqual(format_scalar(None, FormatFlags().with_format_json(True)), "null") + + def test_bool(self) -> None: + self.assertEqual(format_scalar(True, FormatFlags()), "True") + self.assertEqual(format_scalar(False, FormatFlags()), "False") + self.assertEqual(format_scalar(True, FormatFlags().with_format_json(True)), "true") + self.assertEqual(format_scalar(False, FormatFlags().with_format_json(True)), "false") + + def test_string(self) -> None: + self.assertEqual(format_scalar("hello", FormatFlags()), "hello") + self.assertEqual(format_scalar("hello", FormatFlags().with_quote_strings(True)), '"hello"') + self.assertEqual(format_scalar("hello", FormatFlags().with_quote_strings(True).with_single_quotes(True)), "'hello'") + self.assertEqual(format_scalar("hello", FormatFlags().with_use_repr(True)), "hello") + self.assertEqual(format_scalar("hello", FormatFlags().with_use_repr(True).with_quote_strings(True)), '"hello"') + self.assertEqual(format_scalar("k\"l", FormatFlags().with_use_repr(True).with_quote_strings(True)), '"k\\"l"') + + def test_number(self) -> None: + self.assertEqual(format_scalar(123, FormatFlags()), "123") + self.assertEqual(format_scalar(3.14, FormatFlags()), "3.14") + self.assertEqual(format_scalar(123, FormatFlags().with_use_repr(True)), "123") + self.assertEqual(format_scalar(3.14, FormatFlags().with_use_repr(True)), "3.14") + + +class TestSpacer(unittest.TestCase): + def test_single_line(self) -> None: + self.assertEqual(" ", _spacer(FormatFlags().with_single_line(True), 2)) + + def test_multi_line(self) -> None: + self.assertEqual(_spacer(FormatFlags().with_single_line(False), 2), " ") + self.assertEqual(_spacer(FormatFlags().with_single_line(False).with_indent(4), 3), " ") + + +class TestIsEmptyOrSingleItem(unittest.TestCase): + def test_scalar(self) -> None: + self.assertTrue(_is_empty_or_single_item(1)) + self.assertTrue(_is_empty_or_single_item("hello")) + + def test_empty_list(self) -> None: + self.assertTrue(_is_empty_or_single_item([])) + + def test_single_item_list(self) -> None: + self.assertTrue(_is_empty_or_single_item([1])) + self.assertTrue(_is_empty_or_single_item(["hello"])) + self.assertTrue(_is_empty_or_single_item([[]])) + self.assertTrue(_is_empty_or_single_item([[[[]]]])) + self.assertTrue(_is_empty_or_single_item([[[["one"]]]])) + self.assertTrue(_is_empty_or_single_item([{"key": "one"}])) + self.assertTrue(_is_empty_or_single_item([{"key": [["one"]]}])) + self.assertTrue(_is_empty_or_single_item([[{"key": [["foo"]]}]])) + self.assertTrue(_is_empty_or_single_item([{"key1": {"key2": {"key3": "foo"}}}])) + + def test_multi_item_list(self) -> None: + self.assertFalse(_is_empty_or_single_item([1, 2])) + self.assertFalse(_is_empty_or_single_item(["hello", "world"])) + self.assertFalse(_is_empty_or_single_item([["one", "two"]])) + self.assertFalse(_is_empty_or_single_item([[[["one", "two"]]]])) + self.assertFalse(_is_empty_or_single_item([{"key": [["one", "two"]]}])) + self.assertFalse(_is_empty_or_single_item([[{"one": "foo", "two": "bar"}]])) + + def test_empty_dict(self) -> None: + self.assertTrue(_is_empty_or_single_item({})) + + def test_single_item_dict(self) -> None: + self.assertTrue(_is_empty_or_single_item({"key": 1})) + self.assertTrue(_is_empty_or_single_item({"key": "hello"})) + self.assertTrue(_is_empty_or_single_item({"key": []})) + self.assertTrue(_is_empty_or_single_item({"key": [1]})) + self.assertTrue(_is_empty_or_single_item({"key": [["one"]]})) + self.assertTrue(_is_empty_or_single_item({"key": {}})) + self.assertTrue(_is_empty_or_single_item({"key": {"subkey": "value"}})) + self.assertTrue(_is_empty_or_single_item({"key1": {"key2": {"key3": "foo"}}})) + + def test_multi_item_dict(self) -> None: + self.assertFalse(_is_empty_or_single_item({"key1": 1, "key2": 2})) + self.assertFalse(_is_empty_or_single_item({"key": [1, 2]})) + self.assertFalse(_is_empty_or_single_item({"key": {"subkey1": "value1", "subkey2": "value2"}})) + self.assertFalse(_is_empty_or_single_item({"key": [[{"one": "foo", "two": "bar"}]]})) + + +# noinspection SpellCheckingInspection +class TestPPDict(unittest.TestCase): + def test_empty_dict(self) -> None: + lines = [""] + _pp_dict({}, FormatFlags(), lines) + self.assertEqual(lines, ["{ }"]) + + def test_single_scalar_dict(self) -> None: + lines = [""] + _pp_dict({"key": "value"}, FormatFlags(), lines) + self.assertEqual(lines, ["{ key: value }"]) + + def test_multi_scalar_dict(self) -> None: + lines = [""] + _pp_dict({"key1": "value1", "key2": "value2"}, FormatFlags(), lines) + self.assertEqual(["{", " key1: value1,", " key2: value2", " }"],lines) + + def test_nested_list_dict(self) -> None: + lines = [""] + _pp_dict({"key": ["item1", "item2"]}, FormatFlags(), lines) + self.assertEqual(['{', ' key:', ' [', ' item1,', ' item2', ' ]', ' }'], lines) + + def test_nested_dict_dict(self) -> None: + lines = [""] + _pp_dict({"key": {"subkey": "subvalue"}}, FormatFlags().with_single_line(False), lines) + self.assertEqual(['{', ' key: { subkey: subvalue } }'], lines) + + def test_complex_dict(self) -> None: + data: dict[str, JSON_VALUES] = { + "key1": "value1", + "key2": ["item1", "item2"], + "key3": {"subkey1": "subvalue1", "subkey2": "subvalue2"}, + "key4": 123 + } + lines = [""] + _pp_dict(data, FormatFlags().with_single_line(False), lines) + self.assertEqual(["{", " key1: value1,", " key2:", " [", " item1,", " item2", " ],", " key3:", + " {", " subkey1: subvalue1,", " subkey2: subvalue2", " },", " key4: 123", "}"], lines) + + def test_single_line_dict(self) -> None: + lines = [""] + _pp_dict({"key1": "value1", "key2": "value2"}, FormatFlags().with_single_line(True), lines) + self.assertEqual(['{', ' key1: value1,', ' key2: value2', ' }'], lines) + + def test_single_item_list_in_dict(self) -> None: + lines = [""] + _pp_dict({"key": ["item1"]}, FormatFlags(), lines) + self.assertEqual(['{', ' key: [ item1 ] }'], lines) + + def test_single_item_dict_in_dict(self) -> None: + lines = [""] + _pp_dict({"key": {"subkey": "subvalue"}}, FormatFlags(), lines) + self.assertEqual(['{', ' key: { subkey: subvalue } }'], lines) + + def test_single_item_nested_list_in_dict(self) -> None: + lines = [""] + _pp_dict({"key": [["item1"]]}, FormatFlags(), lines) + self.assertEqual(['{', ' key: [ [ item1 ] ] }'], lines) + + def test_single_item_nested_dict_in_dict(self) -> None: + lines = [""] + _pp_dict({"key": [{"subkey": "subvalue"}]}, FormatFlags(), lines) + self.assertEqual(['{', ' key: [ { subkey: subvalue } ] }'], lines) + + def test_single_item_nested_dict_in_dict_2(self) -> None: + lines = [""] + _pp_dict({"key": {"subkey": ["subvalue"]}}, FormatFlags(), lines) + self.assertEqual(['{', ' key:', ' {', ' subkey: [ subvalue ] } }'], lines) + + def test_single_item_nested_dict_in_dict_3(self) -> None: + lines = [""] + _pp_dict({"key": {"subkey": {"subsubkey": "subvalue"}}}, FormatFlags(), lines) + self.assertEqual(['{', ' key:', ' {', ' subkey: { subsubkey: subvalue } } }'], lines) + + def test_single_item_nested_dict_in_dict_4(self) -> None: + lines = [""] + _pp_dict({"key": {"subkey": {"subsubkey": ["subvalue"]}}}, FormatFlags(), lines) + self.assertEqual(['{', ' key:', ' {', ' subkey:', ' {', ' subsubkey: [ subvalue ] } } }'], lines) + + def test_single_item_nested_dict_in_dict_5(self) -> None: + lines = [""] + _pp_dict({"key": {"subkey": {"subsubkey": [{"subsubsubkey": "subvalue"}]}}}, FormatFlags().with_single_line(False), lines) + expected = ['{', + ' key:', + ' {', + ' subkey:', + ' {', + ' subsubkey: [ { subsubsubkey: subvalue } ] } } }'] + self.assertEqual(expected, lines) + + def test_single_item_nested_dict_in_dict_6(self) -> None: + lines = [""] + _pp_dict({"key": {"subkey": {"subsubkey": [{"subsubsubkey": ["subvalue"]}]}}}, FormatFlags(), lines) + expected = ['{', + ' key:', + ' {', + ' subkey:', + ' {', + ' subsubkey: [ {', + ' subsubsubkey: [ subvalue ] } ] } } }'] + self.assertEqual(expected, lines) + + def test_single_item_nested_dict_in_dict_7(self) -> None: + lines = [""] + _pp_dict({"key": {"subkey": {"subsubkey": [{"subsubsubkey": [{"subsubsubsubkey": "subvalue"}]}]}}}, + FormatFlags(), lines) + expected = ['{', + ' key:', + ' {', + ' subkey:', + ' {', + ' subsubkey: [ {', + ' subsubsubkey: [ { subsubsubsubkey: subvalue } ] } ] } } }'] + self.assertEqual(expected, lines) + + def test_single_item_nested_dict_in_dict_8(self) -> None: + lines = [""] + _pp_dict({"key": {"subkey": {"subsubkey": [{"subsubsubkey": [{"subsubsubsubkey": ["subvalue"]}]}]}}}, + FormatFlags().with_single_line(False), lines) + expected = ['{', + ' key:', + ' {', + ' subkey:', + ' {', + ' subsubkey: [ {', + ' subsubsubkey: [ {', + ' subsubsubsubkey: [ subvalue ] } ] } ] } } }'] + self.assertEqual(expected, lines) + + def test_single_item_nested_dict_in_dict_9(self) -> None: + lines = [""] + _pp_dict({"key": { + "subkey": {"subsubkey": [{"subsubsubkey": [{"subsubsubsubkey": [{"subsubsubsubsubkey": "subvalue"}]}]}]}}}, + FormatFlags().with_single_line(False), lines) + expected = ['{', + ' key:', + ' {', + ' subkey:', + ' {', + ' subsubkey: [ {', + ' subsubsubkey: [ {', + ' subsubsubsubkey: [ { subsubsubsubsubkey: subvalue } ] } ] } ] ' + '} } }'] + self.assertEqual(expected, lines) + + def test_single_item_nested_dict_in_dict_10(self) -> None: + lines = [""] + _pp_dict({"key": {"subkey": { + "subsubkey": [{"subsubsubkey": [{"subsubsubsubkey": [{"subsubsubsubsubkey": ["subvalue"]}]}]}]}}}, + FormatFlags().with_single_line(False), lines) + expected = ['{', + ' key:', + ' {', + ' subkey:', + ' {', + ' subsubkey: [ {', + ' subsubsubkey: [ {', + ' subsubsubsubkey: [ {', + ' subsubsubsubsubkey: [ subvalue ] } ] } ] } ] } } }'] + self.assertEqual(expected, lines) + + +# noinspection SpellCheckingInspection +class TestPPList(unittest.TestCase): + def test_empty_list(self) -> None: + lines = [""] + _pp_list([], FormatFlags(), lines) + expected = ["[ ]"] + self.assertEqual(expected,lines ) + + def test_single_scalar_list(self) -> None: + lines = [""] + _pp_list([1], FormatFlags(), lines) + expected = ["[ 1 ]"] + self.assertEqual(expected, lines) + + def test_multi_scalar_list(self) -> None: + lines = [""] + _pp_list([1, 2, 3], FormatFlags().with_single_line(True), lines) + expected = ['[', ' 1,', ' 2,', ' 3', ' ]'] + self.assertEqual(expected, lines) + + def test_nested_list(self) -> None: + lines = [""] + _pp_list([[1, 2], [3, 4]], FormatFlags(), lines) + expected = ['[ [', ' 1,', ' 2', ' ],', ' [', ' 3,', ' 4', ' ]', ' ]'] + self.assertEqual(expected, lines) + + def test_nested_dict_list(self) -> None: + lines = [""] + _pp_list([{"key1": "value1"}, {"key2": "value2"}], FormatFlags(), lines) + expected = ['[ { key1: value1 },', ' { key2: value2 }', ' ]'] + self.assertEqual(expected, lines) + + def test_complex_list(self) -> None: + data: list[JSON_VALUES] = [ + 1, + [2, 3], + {"key1": "value1", "key2": "value2"}, + 4 + ] + lines = [""] + _pp_list(data, FormatFlags().with_single_line(False), lines) + expected = ["[", " 1,", " [", " 2,", " 3", " ],", " {", " key1: value1,", " key2: value2", + " },", " 4", "]"] + self.assertEqual( expected, lines) + + def test_single_line_list(self) -> None: + lines = [""] + _pp_list([1, 2, 3], FormatFlags().with_single_line(True), lines) + expected = ['[', ' 1,', ' 2,', ' 3', ' ]'] + self.assertEqual(expected,lines ) + + def test_single_item_list_in_list(self) -> None: + lines = [""] + _pp_list([[1]], FormatFlags(), lines) + expected = ['[ [ 1 ] ]'] + self.assertEqual(expected, lines ) + + def test_single_item_dict_in_list(self) -> None: + lines = [""] + _pp_list([{"key": "value"}], FormatFlags(), lines) + expected =['[ { key: value } ]'] + self.assertEqual(expected, lines ) + + def test_single_item_nested_list_in_list(self) -> None: + lines = [""] + _pp_list([[[1]]], FormatFlags(), lines) + expected = ['[ [ [ 1 ] ] ]'] + self.assertEqual(expected, lines ) + + def test_single_item_nested_dict_in_list(self) -> None: + lines = [""] + _pp_list([[{"key": "value"}]], FormatFlags(), lines) + expected = ['[ [ { key: value } ] ]'] + self.assertEqual(expected, lines ) + + def test_single_item_nested_dict_in_list_2(self) -> None: + lines = [""] + _pp_list([{"key": [1]}], FormatFlags(), lines) + expected = ['[ {', ' key: [ 1 ] } ]'] + self.assertEqual(expected, lines ) + + def test_single_item_nested_dict_in_list_3(self) -> None: + lines = [""] + _pp_list([{"key": {"subkey": "value"}}], FormatFlags(), lines) + expected = ['[ {', ' key: { subkey: value } } ]'] + self.assertEqual(expected, lines ) + + def test_single_item_nested_dict_in_list_4(self) -> None: + lines = [""] + _pp_list([{"key": {"subkey": [1]}}], FormatFlags(), lines) + expected = ['[ {', ' key:', ' {', ' subkey: [ 1 ] } } ]'] + self.assertEqual(expected, lines ) + + def test_single_item_nested_dict_in_list_5(self) -> None: + lines = [""] + _pp_list([{"key": {"subkey": [{"subsubkey": "value"}]}}], FormatFlags(), lines) + expected = ['[ {', ' key:', ' {', ' subkey: [ { subsubkey: value } ] } } ]'] + self.assertEqual(expected, lines ) + + def test_single_item_nested_dict_in_list_6(self) -> None: + lines = [""] + _pp_list([{"key": {"subkey": [{"subsubkey": [1]}]}}], FormatFlags(), lines) + expected = ['[ {', ' key:', ' {', ' subkey: [ {', ' subsubkey: [ 1 ] } ] } } ]'] + self.assertEqual(expected, lines ) + + def test_single_item_nested_dict_in_list_7(self) -> None: + lines = [""] + _pp_list([{"key": {"subkey": [{"subsubkey": [{"subsubsubkey": "value"}]}]}}], FormatFlags(), lines) + expected = ['[ {', + ' key:', + ' {', + ' subkey: [ {', + ' subsubkey: [ { subsubsubkey: value } ] } ] } } ]'] + self.assertEqual(expected, lines ) + + def test_single_item_nested_dict_in_list_8(self) -> None: + lines = [""] + _pp_list([{"key": {"subkey": [{"subsubkey": [{"subsubsubkey": [1]}]}]}}], FormatFlags(), lines) + expected =['[ {', + ' key:', + ' {', + ' subkey: [ {', + ' subsubkey: [ {', + ' subsubsubkey: [ 1 ] } ] } ] } } ]'] + self.assertEqual(expected, lines ) + + def test_single_item_nested_dict_in_list_9(self) -> None: + lines = [""] + _pp_list([{"key": {"subkey": [{"subsubkey": [{"subsubsubkey": [{"subsubsubsubkey": "value"}]}]}]}}], + FormatFlags(), lines) + expected =['[ {', + ' key:', + ' {', + ' subkey: [ {', + ' subsubkey: [ {', + ' subsubsubkey: [ { subsubsubsubkey: value } ] } ] } ] } } ]'] + self.assertEqual(expected, lines ) + + def test_single_item_nested_dict_in_list_10(self) -> None: + lines = [""] + _pp_list([{"key": {"subkey": [{"subsubkey": [{"subsubsubkey": [{"subsubsubsubkey": [1]}]}]}]}}], FormatFlags(), + lines) + expected = ['[ {', + ' key:', + ' {', + ' subkey: [ {', + ' subsubkey: [ {', + ' subsubsubkey: [ {', + ' subsubsubsubkey: [ 1 ] } ] } ] } ] } } ]'] + self.assertEqual(expected, lines) + + +class TestPrettyPrint(unittest.TestCase): + def test_scalar(self) -> None: + self.assertEqual(pretty_print(1, FormatFlags() ), "1") + self.assertEqual(pretty_print("hello", FormatFlags() ), "hello") + + def test_list(self) -> None: + expected = "[\n 1,\n 2,\n 3\n]" + actual = pretty_print([1, 2, 3], FormatFlags().with_single_line(False)) + self.assertEqual(actual, expected) + + expected = "[ 1, 2, 3 ]" + actual = pretty_print([1, 2, 3], FormatFlags().with_single_line(True)) + self.assertEqual(actual, expected) + + def test_dict(self) -> None: + expected = "{\n key1: value1,\n key2: value2\n}" + actual = pretty_print({"key1": "value1", "key2": "value2"}, FormatFlags().with_single_line(False)) + self.assertEqual( actual, expected ) + + expected = "{ key1: value1, key2: value2 }" + actual = pretty_print({"key1": "value1", "key2": "value2"}, FormatFlags().with_single_line(True)) + self.assertEqual( actual, expected ) + + def test_complex(self) -> None: + data: dict[str, JSON_VALUES] = { + "key1": "value1", + "key2": [1, 2, 3], + "key3": {"subkey1": "subvalue1", "subkey2": "subvalue2"} + } + expected = "{\n key1: value1,\n key2:\n [\n 1,\n 2,\n 3\n ],\n key3:\n {\n subkey1: subvalue1,\n subkey2: subvalue2\n }\n}" + actual = pretty_print(data, FormatFlags().with_single_line(False)) + self.assertEqual(actual, expected) + + def test_complex_single_line(self) -> None: + data: dict[str, JSON_VALUES] = { + "key1": "value1", + "key2": [1, 2, 3], + "key3": {"subkey1": "subvalue1", "subkey2": "subvalue2"} + } + expected = "{ key1: value1, key2: [ 1, 2, 3 ], key3: { subkey1: subvalue1, subkey2: subvalue2 } }" + actual = pretty_print(data, FormatFlags().with_single_line(True)) + self.assertEqual(actual, expected) + + def test_empty_list(self) -> None: + expected = "[ ]" + actual = pretty_print([], FormatFlags(), []) + self.assertEqual(actual, expected) + + def test_empty_dict(self) -> None: + expected = "{ }" + actual = pretty_print({}, FormatFlags()) + self.assertEqual( actual , expected ) + + def test_nested_empty_list(self) -> None: + self.assertEqual(pretty_print([[]], FormatFlags().with_single_line(False)), "[ [ ] ]") + self.assertEqual(pretty_print([[]], FormatFlags().with_single_line(True)), "[ [ ] ]") + + + def test_nested_empty_dict(self) -> None: + self.assertEqual(pretty_print([{}], FormatFlags().with_single_line(False)), "[ { } ]") + self.assertEqual(pretty_print([{}], FormatFlags().with_single_line(True)), "[ { } ]") + + + def test_nested_empty_list_single_line(self) -> None: + self.assertEqual(pretty_print([[]], FormatFlags().with_single_line(True)), "[ [ ] ]") + + def test_nested_empty_dict_single_line(self) -> None: + self.assertEqual(pretty_print([{}], FormatFlags().with_single_line(True)), "[ { } ]") diff --git a/tests/incubator/jsonpointer/test_pretty_printer_cycles.py b/tests/incubator/jsonpointer/test_pretty_printer_cycles.py new file mode 100644 index 0000000..e67c941 --- /dev/null +++ b/tests/incubator/jsonpointer/test_pretty_printer_cycles.py @@ -0,0 +1,95 @@ +# File: test_pretty_printer_cycles.py +# Copyright (c) 2025 Robert L. Ross +# All rights reserved. +# Open-source license to come. +# Created by: Robert L. Ross +# + + + + +"""Test that cycles in the graph being printed are identified and do not cause infinite recursion or stack overflow.""" +import logging +from typing import Any + +from _pytest.logging import LogCaptureFixture + +# noinspection PyProtectedMember +from killerbunny.incubator.jsonpointer.pretty_printer import _pp_list, FormatFlags, _pp_dict + + +# noinspection SpellCheckingInspection +def test_cycle_list_in_list(caplog: LogCaptureFixture) -> None: + parent_list: list[Any] = [ 1 ] + cycle_list = parent_list + parent_list.append(cycle_list) # creates a cycle + + lines = [""] + caplog.set_level(logging.WARN) + actual = _pp_list(parent_list, FormatFlags(), lines) # this should log a warning about the cycle + expected: list[Any] = ['[', ' 1,', ' [...]', ' ]'] + assert actual == expected + + # 1. Check that at least one log message was captured + assert len(caplog.records) == 1 + # 2. Get the first captured record + record = caplog.records[0] + # 3. Assert on the details of the record + assert record.levelname == "WARNING" + assert "Cycle detected in json_list: [1, [...]]" in record.message + +# noinspection SpellCheckingInspection +def test_cycle_dict_in_list(caplog: LogCaptureFixture) -> None: + parent_list: list[Any] = [ 1 ] + dict_: dict[str, Any] = { "one" : 1 } + cycle_dict = dict_ + dict_["two"] = cycle_dict + parent_list.append(dict_) + + lines = [""] + caplog.set_level(logging.WARN) + actual = _pp_list(parent_list, FormatFlags(), lines) # this should log a warning about the cycle + expected: list[Any] = ['[', ' 1,', ' {', ' one: 1,', ' two:', ' {...}', ' }', ' ]'] + assert actual == expected + + assert len(caplog.records) == 1 + record = caplog.records[0] + assert record.levelname == "WARNING" + assert "Cycle detected in json_dict: {'one': 1, 'two': {...}}" in record.message + +# noinspection SpellCheckingInspection +def test_cycle_dict_in_dict(caplog: LogCaptureFixture) -> None: + dict_: dict[str, Any] = { "one" : 1 } + cycle_dict = dict_ + dict_["two"] = cycle_dict + + lines = [""] + caplog.set_level(logging.WARN) + actual = _pp_dict(dict_, FormatFlags(), lines) # this should log a warning about the cycle + expected: list[Any] = ['{', ' one: 1,', ' two:', ' {...}', ' }'] + assert actual == expected + + assert len(caplog.records) == 1 + record = caplog.records[0] + assert record.levelname == "WARNING" + assert "Cycle detected in json_dict: {'one': 1, 'two': {...}}" in record.message + +# noinspection SpellCheckingInspection +def test_cycle_list_in_dict(caplog: LogCaptureFixture) -> None: + list_: list[Any] = [1] + cycle_list = list_ + list_.append(cycle_list) # creates a cycle + dict_: dict[str, Any] = { "one" : 1, "two":list_ } + + lines = [""] + caplog.set_level(logging.WARN) + actual = _pp_dict(dict_, FormatFlags(), lines) # this should log a warning about the cycle + expected: list[Any] = ['{', ' one: 1,', ' two:', ' [', ' 1,', ' [...]', ' ]', ' }'] + assert actual == expected + + assert len(caplog.records) == 1 + record = caplog.records[0] + assert record.levelname == "WARNING" + assert "Cycle detected in json_list: [1, [...]]" in record.message + + From 184cabdb7f928e18461a98f5f6d7b691cb3f419c Mon Sep 17 00:00:00 2001 From: killeroonie Date: Wed, 25 Jun 2025 18:41:18 -0700 Subject: [PATCH 2/4] Add GitHub Actions workflow for running Python tests --- .github/workflows/python-tests.yaml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/python-tests.yaml diff --git a/.github/workflows/python-tests.yaml b/.github/workflows/python-tests.yaml new file mode 100644 index 0000000..c2f9878 --- /dev/null +++ b/.github/workflows/python-tests.yaml @@ -0,0 +1,28 @@ +name: Python Tests + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install pytest + + - name: Run tests + run: | + pytest \ No newline at end of file From 6b1bc74a39c44fe9cda1eecabc2946814429c1d2 Mon Sep 17 00:00:00 2001 From: killeroonie Date: Wed, 25 Jun 2025 19:00:49 -0700 Subject: [PATCH 3/4] Added killerbunny dev install to yaml file fix failing tests. Deleted unused directory (jpath). Deleted unused file main.py. --- .github/workflows/python-tests.yaml | 11 +++++++---- killerbunny/jpath/__init__.py | 6 ------ 2 files changed, 7 insertions(+), 10 deletions(-) delete mode 100644 killerbunny/jpath/__init__.py diff --git a/.github/workflows/python-tests.yaml b/.github/workflows/python-tests.yaml index c2f9878..ae618ac 100644 --- a/.github/workflows/python-tests.yaml +++ b/.github/workflows/python-tests.yaml @@ -16,13 +16,16 @@ jobs: uses: actions/setup-python@v4 with: python-version: '3.12' - + - name: Install dependencies run: | python -m pip install --upgrade pip - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - pip install pytest + # Install the package in development mode + pip install -e . + # Install test dependencies + pip install .[test] + - name: Run tests run: | - pytest \ No newline at end of file + pytest -v \ No newline at end of file diff --git a/killerbunny/jpath/__init__.py b/killerbunny/jpath/__init__.py deleted file mode 100644 index 9393926..0000000 --- a/killerbunny/jpath/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# File: __init__.py -# Copyright (c) 2025 Robert L. Ross -# All rights reserved. -# Open-source license to come. -# Created by: Robert L. Ross -# From e3a34a842dd358431ca1fef615a73a7ec48471da Mon Sep 17 00:00:00 2001 From: killeroonie Date: Wed, 25 Jun 2025 19:01:00 -0700 Subject: [PATCH 4/4] Added killerbunny dev install to yaml file fix failing tests. Deleted unused directory (jpath). Deleted unused file main.py. --- killerbunny/main.py | 43 ------------------------------------------- 1 file changed, 43 deletions(-) delete mode 100644 killerbunny/main.py diff --git a/killerbunny/main.py b/killerbunny/main.py deleted file mode 100644 index 43d9421..0000000 --- a/killerbunny/main.py +++ /dev/null @@ -1,43 +0,0 @@ -# File: main.py -# Copyright (c) 2025 Robert L. Ross -# All rights reserved. -# Open-source license to come. -# Created by: Robert L. Ross -# - -"""A main entry point for temp testing. This project will become a library and -not directly started by a main() method. - -Python Dependencies installed: - -dev/test: -pytest - iniconfig pkgs/main/noarch::iniconfig-1.1.1-pyhd3eb1b0_0 - packaging pkgs/main/osx-64::packaging-24.2-py313hecd8cb5_0 - pluggy pkgs/main/osx-64::pluggy-1.5.0-py313hecd8cb5_0 - pytest pkgs/main/osx-64::pytest-8.3.4-py313hecd8cb5_0 -MyPy -Successfully installed build-1.2.2.post1 pyproject_hooks-1.2.0 - - - - - -Build utils package and deploy to JPathInterpreter: -2.Rebuild the wheel: Navigate to the WordSpy directory and run python -m build --sdist --wheel . again. -This will create a new wheel file in the dist directory. -The version number in the filename might change if you've updated the version in your pyproject.toml file. -3.Reinstall the wheel: Activate your JPathInterpreter environment and install the newly built wheel using -pip install /path/to/WordSpy/dist/utils-X.Y.Z-py3-none-any.whl -(replacing /path/to/WordSpy/dist/utils-X.Y.Z-py3-none-any.whl with the actual path to your new wheel file). - -""" - - - -def main() -> None: - pass - -if __name__ == '__main__': - main() -