Skip to content

Commit

Permalink
iterator and formatters reworked
Browse files Browse the repository at this point in the history
  • Loading branch information
mr-mixas committed Aug 22, 2019
1 parent dec9074 commit 5270834
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 138 deletions.
70 changes: 25 additions & 45 deletions nested_diff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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):
"""
Expand All @@ -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()

Expand All @@ -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):
Expand Down
112 changes: 53 additions & 59 deletions nested_diff/fmt.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,11 @@ def __init__(
self.open_tokens = {
dict: '{',
list: '[',
set: '{',
tuple: '(',
}
self.close_tokens = {
dict: '}',
list: ']',
set: '}',
tuple: ')',
}

Expand All @@ -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',
Expand All @@ -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_):
"""
Expand Down Expand Up @@ -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)
Expand All @@ -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):
"""
Expand Down
10 changes: 8 additions & 2 deletions tests/test_fmt_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
"""\
<set>
Expand Down Expand Up @@ -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 = """\
Expand All @@ -217,6 +220,9 @@ def test_mixed_structures_diff():
[1]
- 3
+ 4
[2]
<set>
+ True
"""

assert expected == got
Loading

0 comments on commit 5270834

Please sign in to comment.