From 5270834fd0a8a3c4460e41bac7ad1ee861419b13 Mon Sep 17 00:00:00 2001 From: Michael Samoglyadov Date: Thu, 22 Aug 2019 08:21:41 +0300 Subject: [PATCH] iterator and formatters reworked --- nested_diff/__init__.py | 70 +++++++++---------------- nested_diff/fmt.py | 112 +++++++++++++++++++--------------------- tests/test_fmt_text.py | 10 +++- tests/test_iterator.py | 60 ++++++++++----------- 4 files changed, 114 insertions(+), 138 deletions(-) diff --git a/nested_diff/__init__.py b/nested_diff/__init__.py index 4c2badf..40b57b4 100644 --- a/nested_diff/__init__.py +++ b/nested_diff/__init__.py @@ -524,26 +524,12 @@ def __init__(self, sort_keys=False): self.sort_keys = sort_keys self.__iters = { - dict: self.iter_mapping, - frozenset: self.iter_set, - list: self.iter_sequence, - set: self.iter_set, - tuple: self.iter_sequence, + dict: self._iter_mapping, + list: self._iter_sequence, + tuple: self._iter_sequence, } - def get_iter(self, type_, value): - """ - Return apropriate iterator for passed value. - - """ - try: - make_iter = self.__iters[type_] - except KeyError: - raise NotImplementedError - - return make_iter(value) - - def iter_mapping(self, value): + def _iter_mapping(self, value): """ Iterate over dict-like objects. @@ -552,10 +538,10 @@ def iter_mapping(self, value): """ items = sorted(value.items()) if self.sort_keys else value.items() for key, val in items: - yield key, val, True + yield value.__class__, key, val @staticmethod - def iter_sequence(value): + def _iter_sequence(value): """ Iterate over lists, tuples and other sequences. @@ -567,20 +553,19 @@ def iter_sequence(value): if 'I' in item: idx = item['I'] - yield idx, item, True + yield value.__class__, idx, item idx += 1 - @staticmethod - def iter_set(value): + def get_iter(self, value): """ - Iterate over set-like objects. - - :param value: set-like object. + Return apropriate iterator for passed diff value. """ - for item in value: - yield None, item, False + try: + return self.__iters[value.__class__](value) + except KeyError: + raise NotImplementedError def set_iter(self, type_, method): """ @@ -589,26 +574,25 @@ def set_iter(self, type_, method): :param type_: data type. :param method: method. - Generator should yield tuples with three items: `pointer`, `value` and - boolean flag `is_pointed`. + Generator should yield tuples with three items: container_type, pointer + and subdiff. """ self.__iters[type_] = method - def iterate(self, ndiff): + def iterate(self, ndiff, depth=0): """ - Return tuples with depth, pointer, subdiff and `is_pointed` boolean - flag for each nested subdiff. + Return tuples with depth, container_type, pointer and subdiff for each + nested diff. :param ndiff: Nested diff. """ - depth = 0 - stack = [((None, _, False) for _ in (ndiff,))] + stack = [((None, None, _) for _ in (ndiff,))] while True: try: - pointer, ndiff, is_pointed = next(stack[-1]) + container_type, pointer, subdiff = next(stack[-1]) except StopIteration: stack.pop() @@ -618,16 +602,12 @@ def iterate(self, ndiff): else: break - yield depth, pointer, ndiff, is_pointed + yield depth, container_type, pointer, subdiff - if 'D' in ndiff: - stack.append( - self.get_iter( - (ndiff['E'] if 'E' in ndiff else ndiff['D']).__class__, - ndiff['D'], - ) - ) - depth += 1 + if 'D' in subdiff: + if 'E' not in subdiff: + stack.append(self.get_iter(subdiff['D'])) + depth += 1 def diff(a, b, **kwargs): diff --git a/nested_diff/fmt.py b/nested_diff/fmt.py index 315f0a6..4152192 100644 --- a/nested_diff/fmt.py +++ b/nested_diff/fmt.py @@ -43,13 +43,11 @@ def __init__( self.open_tokens = { dict: '{', list: '[', - set: '{', tuple: '(', } self.close_tokens = { dict: '}', list: ']', - set: '}', tuple: ')', } @@ -64,6 +62,7 @@ def __init__( self.diff_value_tokens = self.diff_key_tokens.copy() self.tags = ( # diff tags to format, sequence is important + 'D', 'R', 'O', 'N', @@ -76,14 +75,7 @@ def format(self, diff): Return completely formatted diff """ - return ''.join(self.iterate(diff)) - - def iterate(self, diff): - """ - Yield diff token by token - - """ - raise NotImplementedError + return ''.join(self.emit_tokens(diff)) def get_open_token(self, type_): """ @@ -135,64 +127,58 @@ class TextFormatter(AbstractFormatter): Produce human friendly text diff representation with indenting formatting. """ - def iterate(self, diff): + def __init__(self, *args, **kwargs): + super(TextFormatter, self).__init__(*args, **kwargs) + + self.__emitters = { + frozenset: self.emit_set_tokens, + set: self.emit_set_tokens, + } + + def emit_set_tokens(self, diff, depth=0): + yield self.diff_key_tokens['D'] + yield self.indent * depth + yield '<' + yield diff['E'].__class__.__name__ + yield '>' + yield self.line_separator + + depth += 1 + + for subdiff in diff['D']: + for tag in ('R', 'A', 'U'): + if tag in subdiff: + yield self.diff_value_tokens[tag] + yield self.indent * depth + yield self.repr_value(subdiff[tag]) + yield self.line_separator + break + + def get_emitter(self, diff, depth=0): """ - Yield diff token by token + Return apropriate tokens emitter for diff extention. """ - depth = 0 - emit_container_preamble = False - key_tag = 'U' - stack = [((None, _, False) for _ in (diff,))] - path_types = [None] # even with stack - - while True: - try: - pointer, diff, is_pointed = next(stack[-1]) - except StopIteration: - stack.pop() - - if stack: - depth -= 1 - path_types.pop() - container_type = path_types[-1] - continue - else: - break + try: + return self.__emitters[diff['E'].__class__](diff, depth=depth) + except KeyError: + raise NotImplementedError - if 'D' in diff: - if is_pointed: - yield self.diff_key_tokens['D'] - yield self.indent * (depth - 1) - yield self.get_open_token(container_type) - yield self.repr_key(pointer) - yield self.get_close_token(container_type) - yield self.line_separator + def emit_tokens(self, diff, depth=0): + """ + Yield diff token by token + + """ + key_tag = 'D' - container_type = diff['E' if 'E' in diff else 'D'].__class__ - stack.append(self.get_iter(container_type, diff['D'])) - path_types.append(container_type) - emit_container_preamble = True - depth += 1 - continue - - if is_pointed: - key_tag = None - elif emit_container_preamble: # for keyless collections like set - key_tag = 'U' - emit_container_preamble = False - yield self.diff_key_tokens[key_tag] - yield self.indent * (depth - 1) - yield '<' - yield container_type.__name__ - yield '>' - yield self.line_separator + for depth, container_type, pointer, diff in self.iterate( + diff, depth=depth): for tag in self.tags: if tag in diff: + # key/index if key_tag is None: - # key/index - key_tag = tag if tag == 'A' or tag == 'R' else 'U' + key_tag = 'D' if tag in ('O', 'N') else tag yield self.diff_key_tokens[key_tag] yield self.indent * (depth - 1) yield self.get_open_token(container_type) @@ -201,11 +187,19 @@ def iterate(self, diff): yield self.line_separator # value + if tag == 'D': + if 'E' in diff: + for i in self.get_emitter(diff, depth=depth): + yield i + break + yield self.diff_value_tokens[tag] yield self.indent * depth yield self.repr_value(diff[tag]) yield self.line_separator + key_tag = None + class TermFormatter(TextFormatter): """ diff --git a/tests/test_fmt_text.py b/tests/test_fmt_text.py index 9ca8e36..283abce 100644 --- a/tests/test_fmt_text.py +++ b/tests/test_fmt_text.py @@ -161,11 +161,14 @@ def test_dicts_diff_noU(): """ assert expected == got + + def test_sets_diff(): a = {'a',} b = {'a', 'b'} got = TextFormatter().format(diff(a, b)) + expected = { """\ @@ -204,8 +207,8 @@ def test_frozensets_diff(): def test_mixed_structures_diff(): - a = {'one': [{'two': 2}, 3]} - b = {'one': [{'two': 0}, 4]} + a = {'one': [{'two': 2}, 3, set()]} + b = {'one': [{'two': 0}, 4, {True}]} got = TextFormatter().format(diff(a, b)) expected = """\ @@ -217,6 +220,9 @@ def test_mixed_structures_diff(): [1] - 3 + 4 + [2] + ++ True """ assert expected == got diff --git a/tests/test_iterator.py b/tests/test_iterator.py index f19cf81..1b43f87 100644 --- a/tests/test_iterator.py +++ b/tests/test_iterator.py @@ -7,7 +7,7 @@ def test_scalar_diff(): a = 0 b = 1 - expected = [(0, None, {'N': 1, 'O': 0}, False)] + expected = [(0, None, None, {'N': 1, 'O': 0})] got = list(Iterator().iterate(diff(a, b))) assert expected == got @@ -18,13 +18,13 @@ def test_dict_diff(): b = {'1': 1, '2': {'9': 8, '10': 10}, '4': 4} expected = [ - (0, None, {'D': {'1': {'U': 1}, '3': {'R': 3}, '2': {'D': {'9': {'N': 8, 'O': 9}, '10': {'U': 10}}}, '4': {'A': 4}}}, False), - (1, '1', {'U': 1}, True), - (1, '2', {'D': {'9': {'N': 8, 'O': 9}, '10': {'U': 10}}}, True), - (2, '10', {'U': 10}, True), - (2, '9', {'N': 8, 'O': 9}, True), - (1, '3', {'R': 3}, True), - (1, '4', {'A': 4}, True), + (0, None, None, {'D': {'1': {'U': 1}, '3': {'R': 3}, '2': {'D': {'9': {'N': 8, 'O': 9}, '10': {'U': 10}}}, '4': {'A': 4}}}), + (1, dict, '1', {'U': 1}), + (1, dict, '2', {'D': {'9': {'N': 8, 'O': 9}, '10': {'U': 10}}}), + (2, dict, '10', {'U': 10}), + (2, dict, '9', {'N': 8, 'O': 9}), + (1, dict, '3', {'R': 3}), + (1, dict, '4', {'A': 4}), ] got = list(Iterator().iterate(diff(a, b))) @@ -40,13 +40,13 @@ def test_dict_diff_keys_sorted(): b = {'1': 1, '2': {'9': 8, '10': 10}, '4': 4} expected = [ - (0, None, {'D': {'1': {'U': 1}, '3': {'R': 3}, '2': {'D': {'9': {'N': 8, 'O': 9}, '10': {'U': 10}}}, '4': {'A': 4}}}, False), - (1, '1', {'U': 1}, True), - (1, '2', {'D': {'9': {'N': 8, 'O': 9}, '10': {'U': 10}}}, True), - (2, '10', {'U': 10}, True), - (2, '9', {'N': 8, 'O': 9}, True), - (1, '3', {'R': 3}, True), - (1, '4', {'A': 4}, True), + (0, None, None, {'D': {'1': {'U': 1}, '3': {'R': 3}, '2': {'D': {'9': {'N': 8, 'O': 9}, '10': {'U': 10}}}, '4': {'A': 4}}}), + (1, dict, '1', {'U': 1}), + (1, dict, '2', {'D': {'9': {'N': 8, 'O': 9}, '10': {'U': 10}}}), + (2, dict, '10', {'U': 10}), + (2, dict, '9', {'N': 8, 'O': 9}), + (1, dict, '3', {'R': 3}), + (1, dict, '4', {'A': 4}), ] got = list(Iterator(sort_keys=True).iterate(diff(a, b))) @@ -59,12 +59,12 @@ def test_list_diff(): b = [0, [1, 2], 3] expected = [ - (0, None, {'D': [{'U': 0}, {'D': [{'U': 1}, {'A': 2}]}, {'U': 3}]}, False), - (1, 0, {'U': 0}, True), - (1, 1, {'D': [{'U': 1}, {'A': 2}]}, True), - (2, 0, {'U': 1}, True), - (2, 1, {'A': 2}, True), - (1, 2, {'U': 3}, True), + (0, None, None, {'D': [{'U': 0}, {'D': [{'U': 1}, {'A': 2}]}, {'U': 3}]}), + (1, list, 0, {'U': 0}), + (1, list, 1, {'D': [{'U': 1}, {'A': 2}]}), + (2, list, 0, {'U': 1}), + (2, list, 1, {'A': 2}), + (1, list, 2, {'U': 3}), ] got = list(Iterator().iterate(diff(a, b))) @@ -77,9 +77,9 @@ def test_list_diff_noU(): b = [0, [1, 2], 3] expected = [ - (0, None, {'D': [{'D': [{'A': 2, 'I': 1}], 'I': 1}]}, False), - (1, 1, {'D': [{'A': 2, 'I': 1}], 'I': 1}, True), - (2, 1, {'A': 2, 'I': 1}, True), + (0, None, None, {'D': [{'D': [{'A': 2, 'I': 1}], 'I': 1}]}), + (1, list, 1, {'D': [{'A': 2, 'I': 1}], 'I': 1}), + (2, list, 1, {'A': 2, 'I': 1}), ] got = list(Iterator().iterate(diff(a, b, U=False))) @@ -93,11 +93,7 @@ def test_set_diff(): got = list(Iterator().iterate(diff(a, b))) - assert len(got) == 4 - assert got[0] == (0, None, {'D': [{'U': 0}, {'R': 1}, {'A': 2}], 'E': set()}, False) - assert (1, None, {'R': 1}, False) in got - assert (1, None, {'A': 2}, False) in got - assert (1, None, {'U': 0}, False) in got + assert [(0, None, None, {'E': set(), 'D': [{'U': 0}, {'R': 1}, {'A': 2}]})] == got def test_custom_containers(): @@ -107,11 +103,11 @@ class custom_container(tuple): diff = {'D': custom_container([{'O': 0, 'N': 1}])} it = Iterator() - it.set_iter(custom_container, it.iter_sequence) + it.set_iter(custom_container, it._iter_sequence) expected = [ - (0, None, {'D': ({'N': 1, 'O': 0},)}, False), - (1, 0, {'N': 1, 'O': 0}, True) + (0, None, None, {'D': ({'N': 1, 'O': 0},)}), + (1, custom_container, 0, {'N': 1, 'O': 0}) ] got = list(it.iterate(diff))