commit python-jsonpatch for openSUSE:Factory
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 - tbechtold@suse.com + +- 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 <stefan@skoegl.net>' -__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 <mail@example.org>") 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: opensuse-commit+unsubscribe@opensuse.org For additional commands, e-mail: opensuse-commit+help@opensuse.org
participants (1)
-
root@hilbert.suse.de