Hello community,

here is the log from the commit of package python-jsonpatch for 
openSUSE:Factory checked in at 2014-09-18 07:12:46
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-jsonpatch (Old)
 and      /work/SRC/openSUSE:Factory/.python-jsonpatch.new (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-jsonpatch"

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-jsonpatch/python-jsonpatch.changes        
2013-11-24 12:32:18.000000000 +0100
+++ /work/SRC/openSUSE:Factory/.python-jsonpatch.new/python-jsonpatch.changes   
2014-09-18 07:12:48.000000000 +0200
@@ -1,0 +2,15 @@
+Mon Sep 15 09:40:21 UTC 2014 - [email protected]
+
+- update to version 1.7:
+  * bump version to 1.7
+  * [Setup] use utf-8 explicitly in setup.py
+  * bump version to 1.6
+  * Fix make_patch() when root is an array (fixes #28)
+  * Merge branch 'remove-error' of https://github.com/umago/python-json-patch
+  * Improve error message when removing non-existent objects
+  * bump version to 1.5
+  * fix test for Python 3
+  * fix make_patch where obj keys contain "/", fixes #26
+- Update Requires for python-jsonpointer
+
+-------------------------------------------------------------------

Old:
----
  jsonpatch-1.3.tar.gz

New:
----
  jsonpatch-1.7.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-jsonpatch.spec ++++++
--- /var/tmp/diff_new_pack.hbDiT2/_old  2014-09-18 07:12:48.000000000 +0200
+++ /var/tmp/diff_new_pack.hbDiT2/_new  2014-09-18 07:12:48.000000000 +0200
@@ -1,7 +1,7 @@
 #
 # spec file for package python-jsonpatch
 #
-# Copyright (c) 2013 SUSE LINUX Products GmbH, Nuernberg, Germany.
+# Copyright (c) 2014 SUSE LINUX Products GmbH, Nuernberg, Germany.
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -17,7 +17,7 @@
 
 
 Name:           python-jsonpatch
-Version:        1.3
+Version:        1.7
 Release:        0
 Summary:        Python - JSON-Patches
 License:        BSD-3-Clause
@@ -26,7 +26,7 @@
 Source:         
http://pypi.python.org/packages/source/j/jsonpatch/jsonpatch-%{version}.tar.gz
 BuildRequires:  python-devel
 BuildRequires:  python-jsonpointer
-Requires:       python-jsonpointer >= 1.0
+Requires:       python-jsonpointer >= 1.3
 Requires(post): update-alternatives
 Requires(postun): update-alternatives
 BuildRoot:      %{_tmppath}/%{name}-%{version}-build

++++++ jsonpatch-1.3.tar.gz -> jsonpatch-1.7.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonpatch-1.3/PKG-INFO new/jsonpatch-1.7/PKG-INFO
--- old/jsonpatch-1.3/PKG-INFO  2013-10-13 15:14:53.000000000 +0200
+++ new/jsonpatch-1.7/PKG-INFO  2014-07-03 22:07:40.000000000 +0200
@@ -1,6 +1,6 @@
-Metadata-Version: 1.0
+Metadata-Version: 1.1
 Name: jsonpatch
-Version: 1.3
+Version: 1.7
 Summary:  Apply JSON-Patches (RFC 6902) 
 Home-page: https://github.com/stefankoegl/python-json-patch
 Author: Stefan Kögl
@@ -8,3 +8,20 @@
 License: Modified BSD License
 Description: UNKNOWN
 Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: Console
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.2
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Topic :: Software Development :: Libraries
+Classifier: Topic :: Utilities
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonpatch-1.3/README.md new/jsonpatch-1.7/README.md
--- old/jsonpatch-1.3/README.md 2013-10-13 15:06:14.000000000 +0200
+++ new/jsonpatch-1.7/README.md 2013-12-25 12:54:46.000000000 +0100
@@ -11,9 +11,12 @@
 * Website: https://github.com/stefankoegl/python-json-patch
 * Repository: https://github.com/stefankoegl/python-json-patch.git
 * Documentation: https://python-json-patch.readthedocs.org/
+* PyPI: https://pypi.python.org/pypi/jsonpatch
+* Travis-CI: https://travis-ci.org/stefankoegl/python-json-patch
+* Coveralls: https://coveralls.io/r/stefankoegl/python-json-patch
 
 Running external tests
 ----------------------
-To run external tests (such as those from 
https://github.com/json-patch/json-patch-tests) use ext_test.py 
+To run external tests (such as those from 
https://github.com/json-patch/json-patch-tests) use ext_test.py
 
     ./ext_tests.py ../json-patch-tests/tests.json
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonpatch-1.3/bin/jsonpatch 
new/jsonpatch-1.7/bin/jsonpatch
--- old/jsonpatch-1.3/bin/jsonpatch     2013-10-13 15:06:14.000000000 +0200
+++ new/jsonpatch-1.7/bin/jsonpatch     2014-03-22 11:36:01.000000000 +0100
@@ -1,12 +1,11 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 
-from __future__ import print_function
-
 import sys
 import os.path
 import json
 import jsonpatch
+import tempfile
 import argparse
 
 
@@ -15,9 +14,14 @@
 parser.add_argument('ORIGINAL', type=argparse.FileType('r'),
                     help='Original file')
 parser.add_argument('PATCH', type=argparse.FileType('r'),
-                    help='Patch file')
+                    nargs='?', default=sys.stdin,
+                    help='Patch file (read from stdin if omitted)')
 parser.add_argument('--indent', type=int, default=None,
                     help='Indent output by n spaces')
+parser.add_argument('-b', '--backup', action='store_true',
+                    help='Back up ORIGINAL if modifying in-place')
+parser.add_argument('-i', '--in-place', action='store_true',
+                    help='Modify ORIGINAL in-place instead of to stdout')
 parser.add_argument('-v', '--version', action='version',
                     version='%(prog)s ' + jsonpatch.__version__)
 
@@ -35,7 +39,67 @@
     doc = json.load(args.ORIGINAL)
     patch = json.load(args.PATCH)
     result = jsonpatch.apply_patch(doc, patch)
-    print(json.dumps(result, indent=args.indent))
+
+    if args.in_place:
+        dirname = os.path.abspath(os.path.dirname(args.ORIGINAL.name))
+
+        try:
+            # Attempt to replace the file atomically.  We do this by
+            # creating a temporary file in the same directory as the
+            # original file so we can atomically move the new file over
+            # the original later.  (This is done in the same directory
+           # because atomic renames do not work across mount points.)
+
+            fd, pathname = tempfile.mkstemp(dir=dirname)
+            fp = os.fdopen(fd, 'w')
+            atomic = True
+
+        except OSError:
+            # We failed to create the temporary file for an atomic
+            # replace, so fall back to non-atomic mode by backing up
+            # the original (if desired) and writing a new file.
+
+            if args.backup:
+                os.rename(args.ORIGINAL.name, args.ORIGINAL.name + '.orig')
+            fp = open(args.ORIGINAL.name, 'w')
+            atomic = False
+
+    else:
+        # Since we're not replacing the original file in-place, write
+        # the modified JSON to stdout instead.
+
+        fp = sys.stdout
+
+    # By this point we have some sort of file object we can write the 
+    # modified JSON to.
+
+    json.dump(result, fp, indent=args.indent)
+    fp.write('\n')
+
+    if args.in_place:
+        # Close the new file.  If we aren't replacing atomically, this
+        # is our last step, since everything else is already in place.
+
+        fp.close()
+
+        if atomic:
+            try:
+                # Complete the atomic replace by linking the original
+                # to a backup (if desired), fixing up the permissions
+                # on the temporary file, and moving it into place.
+
+                if args.backup:
+                    os.link(args.ORIGINAL.name, args.ORIGINAL.name + '.orig')
+                os.chmod(pathname, os.stat(args.ORIGINAL.name).st_mode)
+                os.rename(pathname, args.ORIGINAL.name)
+
+            except OSError:
+                # In the event we could not actually do the atomic
+                # replace, unlink the original to move it out of the
+                # way and finally move the temporary file into place.
+                
+                os.unlink(args.ORIGINAL.name)
+                os.rename(pathname, args.ORIGINAL.name)
 
 
 if __name__ == "__main__":
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonpatch-1.3/jsonpatch.egg-info/PKG-INFO 
new/jsonpatch-1.7/jsonpatch.egg-info/PKG-INFO
--- old/jsonpatch-1.3/jsonpatch.egg-info/PKG-INFO       2013-10-13 
15:14:53.000000000 +0200
+++ new/jsonpatch-1.7/jsonpatch.egg-info/PKG-INFO       2014-07-03 
22:07:40.000000000 +0200
@@ -1,6 +1,6 @@
-Metadata-Version: 1.0
+Metadata-Version: 1.1
 Name: jsonpatch
-Version: 1.3
+Version: 1.7
 Summary:  Apply JSON-Patches (RFC 6902) 
 Home-page: https://github.com/stefankoegl/python-json-patch
 Author: Stefan Kögl
@@ -8,3 +8,20 @@
 License: Modified BSD License
 Description: UNKNOWN
 Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: Console
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.2
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Topic :: Software Development :: Libraries
+Classifier: Topic :: Utilities
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonpatch-1.3/jsonpatch.egg-info/SOURCES.txt 
new/jsonpatch-1.7/jsonpatch.egg-info/SOURCES.txt
--- old/jsonpatch-1.3/jsonpatch.egg-info/SOURCES.txt    2013-10-13 
15:14:53.000000000 +0200
+++ new/jsonpatch-1.7/jsonpatch.egg-info/SOURCES.txt    2014-07-03 
22:07:40.000000000 +0200
@@ -12,5 +12,6 @@
 jsonpatch.egg-info/PKG-INFO
 jsonpatch.egg-info/SOURCES.txt
 jsonpatch.egg-info/dependency_links.txt
+jsonpatch.egg-info/entry_points.txt
 jsonpatch.egg-info/requires.txt
 jsonpatch.egg-info/top_level.txt
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonpatch-1.3/jsonpatch.egg-info/entry_points.txt 
new/jsonpatch-1.7/jsonpatch.egg-info/entry_points.txt
--- old/jsonpatch-1.3/jsonpatch.egg-info/entry_points.txt       1970-01-01 
01:00:00.000000000 +0100
+++ new/jsonpatch-1.7/jsonpatch.egg-info/entry_points.txt       2014-07-03 
22:07:40.000000000 +0200
@@ -0,0 +1,4 @@
+[console_scripts]
+jsonpatch = jsonpatch:main
+jsondiff = jsondiff:main
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonpatch-1.3/jsonpatch.egg-info/requires.txt 
new/jsonpatch-1.7/jsonpatch.egg-info/requires.txt
--- old/jsonpatch-1.3/jsonpatch.egg-info/requires.txt   2013-10-13 
15:14:53.000000000 +0200
+++ new/jsonpatch-1.7/jsonpatch.egg-info/requires.txt   2014-07-03 
22:07:40.000000000 +0200
@@ -1 +1 @@
-jsonpointer>=1.0
\ No newline at end of file
+jsonpointer>=1.3
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonpatch-1.3/jsonpatch.py 
new/jsonpatch-1.7/jsonpatch.py
--- old/jsonpatch-1.3/jsonpatch.py      2013-10-13 15:13:27.000000000 +0200
+++ new/jsonpatch-1.7/jsonpatch.py      2014-07-03 22:03:46.000000000 +0200
@@ -30,31 +30,32 @@
 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 #
 
+""" Apply JSON-Patches (RFC 6902) """
+
 from __future__ import unicode_literals
 
-""" Apply JSON-Patches (RFC 6902) """
+import collections
+import copy
+import functools
+import inspect
+import itertools
+import json
+import sys
+
+from jsonpointer import JsonPointer, JsonPointerException
 
 # Will be parsed by setup.py to determine package metadata
 __author__ = 'Stefan Kögl <[email protected]>'
-__version__ = '1.3'
+__version__ = '1.7'
 __website__ = 'https://github.com/stefankoegl/python-json-patch'
 __license__ = 'Modified BSD License'
 
-import copy
-import sys
-import operator
-import collections
-
-import json
-
-import jsonpointer
 
+# pylint: disable=E0611,W0404
 if sys.version_info >= (3, 0):
-    basestring = (bytes, str)
+    basestring = (bytes, str)  # pylint: disable=C0103,W0622
 
 
-JsonPointerException = jsonpointer.JsonPointerException
-
 class JsonPatchException(Exception):
     """Base Json Patch exception"""
 
@@ -67,6 +68,7 @@
     - etc.
     """
 
+
 class JsonPatchTestFailed(JsonPatchException, AssertionError):
     """ A Test operation failed """
 
@@ -74,15 +76,15 @@
 def multidict(ordered_pairs):
     """Convert duplicate keys values to lists."""
     # read all values into lists
-    d = collections.defaultdict(list)
-    for k, v in ordered_pairs:
-        d[k].append(v)
-
-    # unpack lists that have only 1 item
-    for k, v in d.items():
-        if len(v) == 1:
-            d[k] = v[0]
-    return dict(d)
+    mdict = collections.defaultdict(list)
+    for key, value in ordered_pairs:
+        mdict[key].append(value)
+
+    return dict(
+        # unpack lists that have only 1 item
+        (key, values[0] if len(values) == 1 else values)
+        for key, values in mdict.items()
+    )
 
 
 def get_loadjson():
@@ -94,9 +96,6 @@
     function with object_pairs_hook set to multidict for Python versions that
     support the parameter. """
 
-    import inspect
-    import functools
-
     argspec = inspect.getargspec(json.load)
     if 'object_pairs_hook' not in argspec.args:
         return json.load
@@ -123,12 +122,14 @@
     :rtype: dict
 
     >>> doc = {'foo': 'bar'}
-    >>> other = apply_patch(doc, [{'op': 'add', 'path': '/baz', 'value': 
'qux'}])
+    >>> patch = [{'op': 'add', 'path': '/baz', 'value': 'qux'}]
+    >>> other = apply_patch(doc, patch)
     >>> doc is not other
     True
     >>> other == {'foo': 'bar', 'baz': 'qux'}
     True
-    >>> apply_patch(doc, [{'op': 'add', 'path': '/baz', 'value': 'qux'}], 
in_place=True) == {'foo': 'bar', 'baz': 'qux'}
+    >>> patch = [{'op': 'add', 'path': '/baz', 'value': 'qux'}]
+    >>> apply_patch(doc, patch, in_place=True) == {'foo': 'bar', 'baz': 'qux'}
     True
     >>> doc == other
     True
@@ -140,6 +141,7 @@
         patch = JsonPatch(patch)
     return patch.apply(doc, in_place)
 
+
 def make_patch(src, dst):
     """Generates patch by comparing of two document objects. Actually is
     a proxy to :meth:`JsonPatch.from_diff` method.
@@ -230,18 +232,16 @@
     def __iter__(self):
         return iter(self.patch)
 
-
     def __hash__(self):
         return hash(tuple(self._ops))
 
-
     def __eq__(self, other):
         if not isinstance(other, JsonPatch):
             return False
+        return self._ops == other._ops
 
-        return len(list(self._ops)) == len(list(other._ops)) and \
-               all(map(operator.eq, self._ops, other._ops))
-
+    def __ne__(self, other):
+        return not(self == other)
 
     @classmethod
     def from_string(cls, patch_str):
@@ -280,41 +280,35 @@
             if value == other:
                 return
             if isinstance(value, dict) and isinstance(other, dict):
-                for operation in compare_dict(path, value, other):
+                for operation in compare_dicts(path, value, other):
                     yield operation
             elif isinstance(value, list) and isinstance(other, list):
-                for operation in compare_list(path, value, other):
+                for operation in compare_lists(path, value, other):
                     yield operation
             else:
-                yield {'op': 'replace', 'path': '/'.join(path), 'value': other}
+                ptr = JsonPointer.from_parts(path)
+                yield {'op': 'replace', 'path': ptr.path, 'value': other}
 
-        def compare_dict(path, src, dst):
+        def compare_dicts(path, src, dst):
             for key in src:
                 if key not in dst:
-                    yield {'op': 'remove', 'path': '/'.join(path + [key])}
+                    ptr = JsonPointer.from_parts(path + [key])
+                    yield {'op': 'remove', 'path': ptr.path}
                     continue
                 current = path + [key]
                 for operation in compare_values(current, src[key], dst[key]):
                     yield operation
             for key in dst:
                 if key not in src:
-                    yield {'op': 'add', 'path': '/'.join(path + [key]), 
'value': dst[key]}
+                    ptr = JsonPointer.from_parts(path + [key])
+                    yield {'op': 'add',
+                           'path': ptr.path,
+                           'value': dst[key]}
 
-        def compare_list(path, src, dst):
-            lsrc, ldst = len(src), len(dst)
-            for idx in range(min(lsrc, ldst)):
-                current = path + [str(idx)]
-                for operation in compare_values(current, src[idx], dst[idx]):
-                    yield operation
-            if lsrc < ldst:
-                for idx in range(lsrc, ldst):
-                    current = path + [str(idx)]
-                    yield {'op': 'add', 'path': '/'.join(current), 'value': 
dst[idx]}
-            elif lsrc > ldst:
-                for idx in reversed(range(ldst, lsrc)):
-                    yield {'op': 'remove', 'path': '/'.join(path + [str(idx)])}
+        def compare_lists(path, src, dst):
+            return _compare_lists(path, src, dst)
 
-        return cls(list(compare_dict([''], src, dst)))
+        return cls(list(compare_values([], src, dst)))
 
     def to_string(self):
         """Returns patch set as JSON string."""
@@ -322,7 +316,7 @@
 
     @property
     def _ops(self):
-        return map(self._get_operation, self.patch)
+        return tuple(map(self._get_operation, self.patch))
 
     def apply(self, obj, in_place=False):
         """Applies the patch to given object.
@@ -355,36 +349,35 @@
             raise JsonPatchException("Operation must be a string")
 
         if op not in self.operations:
-            raise JsonPatchException("Unknown operation '%s'" % op)
+            raise JsonPatchException("Unknown operation {0!r}".format(op))
 
         cls = self.operations[op]
         return cls(operation)
 
 
-
 class PatchOperation(object):
     """A single operation inside a JSON Patch."""
 
     def __init__(self, operation):
         self.location = operation['path']
-        self.pointer = jsonpointer.JsonPointer(self.location)
+        self.pointer = JsonPointer(self.location)
         self.operation = operation
 
     def apply(self, obj):
         """Abstract method that applies patch operation to specified object."""
         raise NotImplementedError('should implement patch operation.')
 
-
     def __hash__(self):
         return hash(frozenset(self.operation.items()))
 
-
     def __eq__(self, other):
         if not isinstance(other, PatchOperation):
             return False
-
         return self.operation == other.operation
 
+    def __ne__(self, other):
+        return not(self == other)
+
 
 class RemoveOperation(PatchOperation):
     """Removes an object property or an array element."""
@@ -393,8 +386,9 @@
         subobj, part = self.pointer.to_last(obj)
         try:
             del subobj[part]
-        except IndexError as ex:
-            raise JsonPatchConflict(str(ex))
+        except (KeyError, IndexError) as ex:
+            msg = "can't remove non-existent object '{0}'".format(part)
+            raise JsonPatchConflict(msg)
 
         return obj
 
@@ -406,30 +400,25 @@
         value = self.operation["value"]
         subobj, part = self.pointer.to_last(obj)
 
-        # type is already checked in to_last(), so we assert here
-        # for consistency
-        assert isinstance(subobj, list) or isinstance(subobj, dict), \
-            "invalid document type %s" (type(doc),)
-
         if isinstance(subobj, list):
-
             if part == '-':
-                subobj.append(value)
+                subobj.append(value)  # pylint: disable=E1103
 
             elif part > len(subobj) or part < 0:
                 raise JsonPatchConflict("can't insert outside of list")
 
             else:
-                subobj.insert(part, value)
+                subobj.insert(part, value)  # pylint: disable=E1103
 
         elif isinstance(subobj, dict):
             if part is None:
-                # we're replacing the root
-                obj = value
-
+                obj = value  # we're replacing the root
             else:
                 subobj[part] = value
 
+        else:
+            raise TypeError("invalid document type {0}".format(type(subobj)))
+
         return obj
 
 
@@ -440,11 +429,6 @@
         value = self.operation["value"]
         subobj, part = self.pointer.to_last(obj)
 
-        # type is already checked in to_last(), so we assert here
-        # for consistency
-        assert isinstance(subobj, list) or isinstance(subobj, dict), \
-            "invalid document type %s" (type(doc),)
-
         if part is None:
             return value
 
@@ -454,8 +438,10 @@
 
         elif isinstance(subobj, dict):
             if not part in subobj:
-                raise JsonPatchConflict("can't replace non-existant object 
'%s'"
-                                        "" % part)
+                msg = "can't replace non-existent object '{0}'".format(part)
+                raise JsonPatchConflict(msg)
+        else:
+            raise TypeError("invalid document type {0}".format(type(subobj)))
 
         subobj[part] = value
         return obj
@@ -465,15 +451,27 @@
     """Moves an object property or an array element to new location."""
 
     def apply(self, obj):
-        from_ptr = jsonpointer.JsonPointer(self.operation['from'])
+        from_ptr = JsonPointer(self.operation['from'])
         subobj, part = from_ptr.to_last(obj)
-        value = subobj[part]
+        try:
+            value = subobj[part]
+        except (KeyError, IndexError) as ex:
+            raise JsonPatchConflict(str(ex))
 
-        if self.pointer.contains(from_ptr):
+        if isinstance(subobj, dict) and self.pointer.contains(from_ptr):
             raise JsonPatchException('Cannot move values into its own 
children')
 
-        obj = RemoveOperation({'op': 'remove', 'path': 
self.operation['from']}).apply(obj)
-        obj = AddOperation({'op': 'add', 'path': self.location, 'value': 
value}).apply(obj)
+        obj = RemoveOperation({
+            'op': 'remove',
+            'path': self.operation['from']
+        }).apply(obj)
+
+        obj = AddOperation({
+            'op': 'add',
+            'path': self.location,
+            'value': value
+        }).apply(obj)
+
         return obj
 
 
@@ -487,14 +485,15 @@
                 val = subobj
             else:
                 val = self.pointer.walk(subobj, part)
-
         except JsonPointerException as ex:
             raise JsonPatchTestFailed(str(ex))
 
         if 'value' in self.operation:
             value = self.operation['value']
             if val != value:
-                raise JsonPatchTestFailed('%s is not equal to tested value %s 
(types %s and %s)' % (val, value, type(val), type(value)))
+                msg = '{0} ({1}) is not equal to tested value {2} ({3})'
+                raise JsonPatchTestFailed(msg.format(val, type(val),
+                                                     value, type(value)))
 
         return obj
 
@@ -503,8 +502,245 @@
     """ Copies an object property or an array element to a new location """
 
     def apply(self, obj):
-        from_ptr = jsonpointer.JsonPointer(self.operation['from'])
+        from_ptr = JsonPointer(self.operation['from'])
         subobj, part = from_ptr.to_last(obj)
-        value = copy.deepcopy(subobj[part])
-        obj = AddOperation({'op': 'add', 'path': self.location, 'value': 
value}).apply(obj)
+        try:
+            value = copy.deepcopy(subobj[part])
+        except (KeyError, IndexError) as ex:
+            raise JsonPatchConflict(str(ex))
+
+        obj = AddOperation({
+            'op': 'add',
+            'path': self.location,
+            'value': value
+        }).apply(obj)
+
         return obj
+
+
+def _compare_lists(path, src, dst):
+    """Compares two lists objects and return JSON patch about."""
+    return _optimize(_compare(path, src, dst, *_split_by_common_seq(src, dst)))
+
+
+def _longest_common_subseq(src, dst):
+    """Returns pair of ranges of longest common subsequence for the `src`
+    and `dst` lists.
+
+    >>> src = [1, 2, 3, 4]
+    >>> dst = [0, 1, 2, 3, 5]
+    >>> # The longest common subsequence for these lists is [1, 2, 3]
+    ... # which is located at (0, 3) index range for src list and (1, 4) for
+    ... # dst one. Tuple of these ranges we should get back.
+    ... assert ((0, 3), (1, 4)) == _longest_common_subseq(src, dst)
+    """
+    lsrc, ldst = len(src), len(dst)
+    drange = list(range(ldst))
+    matrix = [[0] * ldst for _ in range(lsrc)]
+    z = 0  # length of the longest subsequence
+    range_src, range_dst = None, None
+    for i, j in itertools.product(range(lsrc), drange):
+        if src[i] == dst[j]:
+            if i == 0 or j == 0:
+                matrix[i][j] = 1
+            else:
+                matrix[i][j] = matrix[i-1][j-1] + 1
+            if matrix[i][j] > z:
+                z = matrix[i][j]
+            if matrix[i][j] == z:
+                range_src = (i-z+1, i+1)
+                range_dst = (j-z+1, j+1)
+        else:
+            matrix[i][j] = 0
+    return range_src, range_dst
+
+
+def _split_by_common_seq(src, dst, bx=(0, -1), by=(0, -1)):
+    """Recursively splits the `dst` list onto two parts: left and right.
+    The left part contains differences on left from common subsequence,
+    same as the right part by for other side.
+
+    To easily understand the process let's take two lists: [0, 1, 2, 3] as
+    `src` and [1, 2, 4, 5] for `dst`. If we've tried to generate the binary 
tree
+    where nodes are common subsequence for both lists, leaves on the left
+    side are subsequence for `src` list and leaves on the right one for `dst`,
+    our tree would looks like::
+
+        [1, 2]
+       /     \
+    [0]       []
+             /  \
+          [3]   [4, 5]
+
+    This function generate the similar structure as flat tree, but without
+    nodes with common subsequences - since we're don't need them - only with
+    left and right leaves::
+
+        []
+       / \
+    [0]  []
+        / \
+     [3]  [4, 5]
+
+    The `bx` is the absolute range for currently processed subsequence of
+    `src` list.  The `by` means the same, but for the `dst` list.
+    """
+    # Prevent useless comparisons in future
+    bx = bx if bx[0] != bx[1] else None
+    by = by if by[0] != by[1] else None
+
+    if not src:
+        return [None, by]
+    elif not dst:
+        return [bx, None]
+
+    # note that these ranges are relative for processed sublists
+    x, y = _longest_common_subseq(src, dst)
+
+    if x is None or y is None:  # no more any common subsequence
+        return [bx, by]
+
+    return [_split_by_common_seq(src[:x[0]], dst[:y[0]],
+                                 (bx[0], bx[0] + x[0]),
+                                 (by[0], by[0] + y[0])),
+            _split_by_common_seq(src[x[1]:], dst[y[1]:],
+                                 (bx[0] + x[1], bx[0] + len(src)),
+                                 (bx[0] + y[1], bx[0] + len(dst)))]
+
+
+def _compare(path, src, dst, left, right):
+    """Same as :func:`_compare_with_shift` but strips emitted `shift` value."""
+    for op, _ in _compare_with_shift(path, src, dst, left, right, 0):
+        yield op
+
+
+def _compare_with_shift(path, src, dst, left, right, shift):
+    """Recursively compares differences from `left` and `right` sides
+    from common subsequences.
+
+    The `shift` parameter is used to store index shift which caused
+    by ``add`` and ``remove`` operations.
+
+    Yields JSON patch operations and list index shift.
+    """
+    if isinstance(left, list):
+        for item, shift in _compare_with_shift(path, src, dst, *left,
+                                               shift=shift):
+            yield item, shift
+    elif left is not None:
+        for item, shift in _compare_left(path, src, left, shift):
+            yield item, shift
+
+    if isinstance(right, list):
+        for item, shift in _compare_with_shift(path, src, dst, *right,
+                                               shift=shift):
+            yield item, shift
+    elif right is not None:
+        for item, shift in _compare_right(path, dst, right, shift):
+            yield item, shift
+
+
+def _compare_left(path, src, left, shift):
+    """Yields JSON patch ``remove`` operations for elements that are only
+    exists in the `src` list."""
+    start, end = left
+    if end == -1:
+        end = len(src)
+    # we need to `remove` elements from list tail to not deal with index shift
+    for idx in reversed(range(start + shift, end + shift)):
+        ptr = JsonPointer.from_parts(path + [str(idx)])
+        yield (
+            {'op': 'remove',
+             # yes, there should be any value field, but we'll use it
+             # to apply `move` optimization a bit later and will remove
+             # it in _optimize function.
+             'value': src[idx - shift],
+             'path': ptr.path,
+            },
+            shift - 1
+        )
+        shift -= 1
+
+
+def _compare_right(path, dst, right, shift):
+    """Yields JSON patch ``add`` operations for elements that are only
+    exists in the `dst` list"""
+    start, end = right
+    if end == -1:
+        end = len(dst)
+    for idx in range(start, end):
+        ptr = JsonPointer.from_parts(path + [str(idx)])
+        yield (
+            {'op': 'add', 'path': ptr.path, 'value': dst[idx]},
+            shift + 1
+        )
+        shift += 1
+
+
+def _optimize(operations):
+    """Optimizes operations which was produced by lists comparison.
+
+    Actually it does two kinds of optimizations:
+
+    1. Seeks pair of ``remove`` and ``add`` operations against the same path
+       and replaces them with ``replace`` operation.
+    2. Seeks pair of ``remove`` and ``add`` operations for the same value
+       and replaces them with ``move`` operation.
+    """
+    result = []
+    ops_by_path = {}
+    ops_by_value = {}
+    add_remove = set(['add', 'remove'])
+    for item in operations:
+        # could we apply "move" optimization for dict values?
+        hashable_value = not isinstance(item['value'], (dict, list))
+        if item['path'] in ops_by_path:
+            _optimize_using_replace(ops_by_path[item['path']], item)
+            continue
+        if hashable_value and item['value'] in ops_by_value:
+            prev_item = ops_by_value[item['value']]
+            # ensure that we processing pair of add-remove ops
+            if set([item['op'], prev_item['op']]) == add_remove:
+                _optimize_using_move(prev_item, item)
+                ops_by_value.pop(item['value'])
+                continue
+        result.append(item)
+        ops_by_path[item['path']] = item
+        if hashable_value:
+            ops_by_value[item['value']] = item
+
+    # cleanup
+    ops_by_path.clear()
+    ops_by_value.clear()
+    for item in result:
+        if item['op'] == 'remove':
+            item.pop('value')  # strip our hack
+        yield item
+
+
+def _optimize_using_replace(prev, cur):
+    """Optimises JSON patch by using ``replace`` operation instead of
+    ``remove`` and ``add`` against the same path."""
+    prev['op'] = 'replace'
+    if cur['op'] == 'add':
+        prev['value'] = cur['value']
+
+
+def _optimize_using_move(prev_item, item):
+    """Optimises JSON patch by using ``move`` operation instead of
+    ``remove` and ``add`` against the different paths but for the same 
value."""
+    prev_item['op'] = 'move'
+    move_from, move_to = [
+        (item['path'], prev_item['path']),
+        (prev_item['path'], item['path']),
+    ][item['op'] == 'add']
+    if item['op'] == 'add':  # first was remove then add
+        prev_item['from'] = move_from
+        prev_item['path'] = move_to
+    else:  # first was add then remove
+        head, move_from = move_from.rsplit('/', 1)
+        # since add operation was first it incremented
+        # overall index shift value. we have to fix this
+        move_from = int(move_from) - 1
+        prev_item['from'] = head + '/%d' % move_from
+        prev_item['path'] = move_to
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonpatch-1.3/requirements.txt 
new/jsonpatch-1.7/requirements.txt
--- old/jsonpatch-1.3/requirements.txt  2013-10-13 15:06:14.000000000 +0200
+++ new/jsonpatch-1.7/requirements.txt  2014-06-24 20:17:59.000000000 +0200
@@ -1 +1 @@
-jsonpointer>=1.0
+jsonpointer>=1.3
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonpatch-1.3/setup.py new/jsonpatch-1.7/setup.py
--- old/jsonpatch-1.3/setup.py  2013-10-13 15:06:14.000000000 +0200
+++ new/jsonpatch-1.7/setup.py  2014-06-30 22:41:31.000000000 +0200
@@ -1,6 +1,7 @@
 #!/usr/bin/env python
 
 import sys
+import io
 import re
 import warnings
 try:
@@ -10,7 +11,7 @@
     from distutils.core import setup
     has_setuptools = False
 
-src = open('jsonpatch.py').read()
+src = io.open('jsonpatch.py', encoding='utf-8').read()
 metadata = dict(re.findall("__([a-z]+)__ = '([^']+)'", src))
 docstrings = re.findall('"""([^"]*)"""', src, re.MULTILINE | re.DOTALL)
 
@@ -43,6 +44,27 @@
 # Extract name and e-mail ("Firstname Lastname <[email protected]>")
 AUTHOR, EMAIL = re.match(r'(.*) <(.*)>', AUTHOR_EMAIL).groups()
 
+CLASSIFIERS = [
+    'Development Status :: 5 - Production/Stable',
+    'Environment :: Console',
+    'Intended Audience :: Developers',
+    'License :: OSI Approved :: BSD License',
+    'Operating System :: OS Independent',
+    'Programming Language :: Python',
+    'Programming Language :: Python :: 2',
+    'Programming Language :: Python :: 2.6',
+    'Programming Language :: Python :: 2.7',
+    'Programming Language :: Python :: 3',
+    'Programming Language :: Python :: 3.2',
+    'Programming Language :: Python :: 3.3',
+    'Programming Language :: Python :: 3.4',
+    'Programming Language :: Python :: Implementation :: CPython',
+    'Programming Language :: Python :: Implementation :: PyPy',
+    'Topic :: Software Development :: Libraries',
+    'Topic :: Utilities',
+]
+
+
 setup(name=PACKAGE,
       version=VERSION,
       description=DESCRIPTION,
@@ -53,10 +75,11 @@
       py_modules=MODULES,
       package_data={'': ['requirements.txt']},
       scripts=['bin/jsondiff', 'bin/jsonpatch'],
-      entry_poimts = {
+      entry_points = {
         'console_scripts': [
             'jsondiff = jsondiff:main',
             'jsonpatch = jsonpatch:main',
         ]},
+      classifiers=CLASSIFIERS,
       **OPTIONS
 )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/jsonpatch-1.3/tests.py new/jsonpatch-1.7/tests.py
--- old/jsonpatch-1.3/tests.py  2013-10-13 15:06:14.000000000 +0200
+++ new/jsonpatch-1.7/tests.py  2014-06-24 20:30:31.000000000 +0200
@@ -75,6 +75,12 @@
                                            'value': 'boo'}])
         self.assertEqual(res['foo'], ['bar', 'boo', 'baz'])
 
+    def test_move_object_keyerror(self):
+        obj = {'foo': {'bar': 'baz'},
+               'qux': {'corge': 'grault'}}
+        patch_obj = [ {'op': 'move', 'from': '/foo/non-existent', 'path': 
'/qux/thud'} ]
+        self.assertRaises(jsonpatch.JsonPatchConflict, jsonpatch.apply_patch, 
obj, patch_obj)
+
     def test_move_object_key(self):
         obj = {'foo': {'bar': 'baz', 'waldo': 'fred'},
                'qux': {'corge': 'grault'}}
@@ -88,6 +94,18 @@
         res = jsonpatch.apply_patch(obj, [{'op': 'move', 'from': '/foo/1', 
'path': '/foo/3'}])
         self.assertEqual(res, {'foo': ['all', 'cows', 'eat', 'grass']})
 
+    def test_move_array_item_into_other_item(self):
+        obj = [{"foo": []}, {"bar": []}]
+        patch = [{"op": "move", "from": "/0", "path": "/0/bar/0"}]
+        res = jsonpatch.apply_patch(obj, patch)
+        self.assertEqual(res, [{'bar': [{"foo": []}]}])
+
+    def test_copy_object_keyerror(self):
+        obj = {'foo': {'bar': 'baz'},
+               'qux': {'corge': 'grault'}}
+        patch_obj = [{'op': 'copy', 'from': '/foo/non-existent', 'path': 
'/qux/thud'}]
+        self.assertRaises(jsonpatch.JsonPatchConflict, jsonpatch.apply_patch, 
obj, patch_obj)
+
     def test_copy_object_key(self):
         obj = {'foo': {'bar': 'baz', 'waldo': 'fred'},
                'qux': {'corge': 'grault'}}
@@ -279,6 +297,58 @@
                    }
         self.assertEqual(expected, res)
 
+    def test_should_just_add_new_item_not_rebuild_all_list(self):
+        src = {'foo': [1, 2, 3]}
+        dst = {'foo': [3, 1, 2, 3]}
+        patch = list(jsonpatch.make_patch(src, dst))
+        self.assertEqual(len(patch), 1)
+        self.assertEqual(patch[0]['op'], 'add')
+        res = jsonpatch.apply_patch(src, patch)
+        self.assertEqual(res, dst)
+
+    def test_use_replace_instead_of_remove_add(self):
+        src = {'foo': [1, 2, 3]}
+        dst = {'foo': [3, 2, 3]}
+        patch = list(jsonpatch.make_patch(src, dst))
+        self.assertEqual(len(patch), 1)
+        self.assertEqual(patch[0]['op'], 'replace')
+        res = jsonpatch.apply_patch(src, patch)
+        self.assertEqual(res, dst)
+
+    def test_use_move_instead_of_remove_add(self):
+        src = {'foo': [4, 1, 2, 3]}
+        dst = {'foo': [1, 2, 3, 4]}
+        patch = list(jsonpatch.make_patch(src, dst))
+        self.assertEqual(len(patch), 1)
+        self.assertEqual(patch[0]['op'], 'move')
+        res = jsonpatch.apply_patch(src, patch)
+        self.assertEqual(res, dst)
+
+    def test_use_move_instead_of_add_remove(self):
+        src = {'foo': [1, 2, 3]}
+        dst = {'foo': [3, 1, 2]}
+        patch = list(jsonpatch.make_patch(src, dst))
+        self.assertEqual(len(patch), 1)
+        self.assertEqual(patch[0]['op'], 'move')
+        res = jsonpatch.apply_patch(src, patch)
+        self.assertEqual(res, dst)
+
+    def test_escape(self):
+        src = {"x/y": 1}
+        dst = {"x/y": 2}
+        patch = jsonpatch.make_patch(src, dst)
+        self.assertEqual([{"path": "/x~1y", "value": 2, "op": "replace"}], 
patch.patch)
+        res = patch.apply(src)
+        self.assertEqual(res, dst)
+
+    def test_root_list(self):
+        """ Test making and applying a patch of the root is a list """
+        src = [{'foo': 'bar', 'boo': 'qux'}]
+        dst = [{'baz': 'qux', 'foo': 'boo'}]
+        patch = jsonpatch.make_patch(src, dst)
+        res = patch.apply(src)
+        self.assertEqual(res, dst)
+
 
 class InvalidInputTests(unittest.TestCase):
 
@@ -308,6 +378,11 @@
         patch_obj = [ { "op": "remove", "path": "/foo/b"} ]
         self.assertRaises(jsonpointer.JsonPointerException, 
jsonpatch.apply_patch, src, patch_obj)
 
+    def test_remove_keyerror_dict(self):
+        src = {'foo': {'bar': 'barz'}}
+        patch_obj = [ { "op": "remove", "path": "/foo/non-existent"} ]
+        self.assertRaises(jsonpatch.JsonPatchConflict, jsonpatch.apply_patch, 
src, patch_obj)
+
     def test_insert_oob(self):
         src = {"foo": [1, 2]}
         patch_obj = [ { "op": "add", "path": "/foo/10", "value": 1} ]

-- 
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to