src/pyams_zodbbrowser/diff.py
changeset 0 a02202f95e2c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser/diff.py	Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,136 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+from html import escape
+
+# import interfaces
+from pyams_zodbbrowser.interfaces import IValueRenderer
+
+# import packages
+
+
+ADDED = 'added'
+REMOVED = 'removed'
+CHANGED = 'changed to'
+
+
+def compareDicts(new, old):
+    """Compare two state dictionaries, return dict."""
+    diffs = {}
+    for key, value in list(new.items()):
+        if key not in old:
+            diffs[key] = (ADDED, value, None)
+        elif old[key] != value:
+            diffs[key] = (CHANGED, value, old[key])
+    for key, value in list(old.items()):
+        if key not in new:
+            diffs[key] = (REMOVED, None, value)
+    return diffs
+
+
+def isascii(s):
+    """See if the string can be safely converted to unicode."""
+    try:
+        s.encode('ascii')
+    except UnicodeError:
+        return False
+    else:
+        return True
+
+
+def compareTuples(new, old):
+    """Compare two tuples.
+
+    Return (common_prefix, middle_of_old, middle_of_new, common_suffix)
+    """
+    first = 0
+    for oldval, newval in zip(old, new):
+        if oldval != newval:
+            break
+        first += 1
+    last = 0
+    for oldval, newval in zip(reversed(old[first:]), reversed(new[first:])):
+        if oldval != newval:
+            break
+        last += 1
+    return (old[:first],
+            old[first:len(old) - last],
+            new[first:len(new) - last],
+            old[len(old) - last:])
+
+
+def compareTuplesHTML(new, old, tid=None, indent=''):
+    """Compare two tuples, return HTML."""
+    html = [indent + '<div class="diff">\n']
+    prefix, removed, added, suffix = compareTuples(new, old)
+    if len(prefix) > 0:
+        html.append(indent + '  <div class="diffitem %s">\n' % 'same')
+        if len(prefix) == 1:
+            html.append(indent + '    first item kept the same\n')
+        else:
+            html.append(indent + '    first %d items kept the same\n' % len(prefix))
+        html.append(indent + '  </div>\n')
+    for oldval in removed:
+        html.append(indent + '  <div class="diffitem %s">\n' % REMOVED)
+        html.append(indent + '    %s %s\n' % (
+            REMOVED, IValueRenderer(oldval).render(tid)))
+        html.append(indent + '  </div>\n')
+    for newval in added:
+        html.append(indent + '  <div class="diffitem %s">\n' % ADDED)
+        html.append(indent + '    %s %s\n' % (
+            ADDED, IValueRenderer(newval).render(tid)))
+        html.append(indent + '  </div>\n')
+    if len(suffix) > 0:
+        html.append(indent + '  <div class="diffitem %s">\n' % 'same')
+        if len(suffix) == 1:
+            html.append(indent + '    last item kept the same\n')
+        else:
+            html.append(indent + '    last %d items kept the same\n' % len(suffix))
+        html.append(indent + '  </div>\n')
+    html.append(indent + '</div>\n')
+    return ''.join(html)
+
+
+def compareDictsHTML(new, old, tid=None, indent=''):
+    """Compare two state dictionaries, return HTML."""
+    html = [indent + '<div class="diff">\n']
+    diff = compareDicts(new, old)
+    for key, (action, newvalue, oldvalue) in sorted(list(diff.items()),
+                                                    key=lambda k_v: (str(type(k_v[0])), k_v[0])):
+        what = action.split()[0]
+        html.append(indent + '  <div class="diffitem %s">\n' % escape(what))
+        if isinstance(key, str) and isascii(key):
+            html.append(indent + '    <strong>%s</strong>: ' % escape(key))
+        else:
+            html.append(indent + '    <strong>%s</strong>: ' % IValueRenderer(key).render(tid))
+        if (action == CHANGED and isinstance(oldvalue, dict) and isinstance(newvalue, dict)):
+            html.append('dictionary changed:\n')
+            html.append(compareDictsHTML(newvalue, oldvalue, tid, indent=indent + '    '))
+        elif (action == CHANGED and isinstance(oldvalue, tuple) and isinstance(newvalue, tuple)):
+            html.append('tuple changed:\n')
+            html.append(compareTuplesHTML(newvalue, oldvalue, tid, indent=indent + '    '))
+        else:
+            html.append(action)
+            html.append(' ')
+            if action == REMOVED:
+                value = oldvalue
+            else:
+                value = newvalue
+            html.append(IValueRenderer(value).render(tid))
+            html.append('\n')
+        html.append(indent + '  </div>\n')
+    html.append(indent + '</div>\n')
+    return ''.join(html)