From e99d178396f69f8891a62e21434c2783b76146b2 Mon Sep 17 00:00:00 2001 From: Christian Lyder Jacobsen Date: Fri, 31 Jan 2020 11:58:59 +0100 Subject: [PATCH 01/47] Make it possible for from_diff to support custom types (issue #107) --- jsonpatch.py | 13 +++++++++---- tests.py | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/jsonpatch.py b/jsonpatch.py index 7f31ce5..ebaa8e3 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -258,7 +258,7 @@ def from_string(cls, patch_str): return cls(patch) @classmethod - def from_diff(cls, src, dst, optimization=True): + def from_diff(cls, src, dst, optimization=True, dumps=json.dumps): """Creates JsonPatch instance based on comparing of two document objects. Json patch would be created for `src` argument against `dst` one. @@ -269,6 +269,10 @@ def from_diff(cls, src, dst, optimization=True): :param dst: Data source document object. :type dst: dict + :param dumps: A function of one argument that produces a serialized + JSON string. + :type dumps: function + :return: :class:`JsonPatch` instance. >>> src = {'foo': 'bar', 'numbers': [1, 3, 4, 8]} @@ -279,7 +283,7 @@ def from_diff(cls, src, dst, optimization=True): True """ - builder = DiffBuilder() + builder = DiffBuilder(dumps) builder._compare_values('', None, src, dst) ops = list(builder.execute()) return cls(ops) @@ -637,7 +641,8 @@ def apply(self, obj): class DiffBuilder(object): - def __init__(self): + def __init__(self, dumps): + self.dumps = dumps self.index_storage = [{}, {}] self.index_storage2 = [[], []] self.__root = root = [] @@ -832,7 +837,7 @@ def _compare_values(self, path, key, src, dst): # and ignore those that don't. The performance of this could be # improved by doing more direct type checks, but we'd need to be # careful to accept type changes that don't matter when JSONified. - elif json.dumps(src) == json.dumps(dst): + elif self.dumps(src) == self.dumps(dst): return else: diff --git a/tests.py b/tests.py index cde90b0..8837bfa 100755 --- a/tests.py +++ b/tests.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import json +import decimal import doctest import unittest import jsonpatch @@ -445,6 +446,20 @@ def test_issue103(self): self.assertEqual(res, dst) self.assertIsInstance(res['A'], float) + def test_custom_types(self): + def default(obj): + if isinstance(obj, decimal.Decimal): + return str(obj) + raise TypeError('Unknown type') + + def dumps(obj): + return json.dumps(obj, default=default) + + old = {'value': decimal.Decimal('1.0')} + new = {'value': decimal.Decimal('1.00')} + patch = jsonpatch.JsonPatch.from_diff(old, new, dumps=dumps) + new_from_patch = jsonpatch.apply_patch(old, patch) + self.assertEqual(new, new_from_patch) class OptimizationTests(unittest.TestCase): From 8fbed9b38648f922bbbdb6984d0fd87b7227b149 Mon Sep 17 00:00:00 2001 From: vavanade Date: Thu, 27 Feb 2020 17:08:47 +0100 Subject: [PATCH 02/47] Fixed some typos and wording --- jsonpatch.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/jsonpatch.py b/jsonpatch.py index 7f31ce5..ca22e34 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -76,9 +76,9 @@ class InvalidJsonPatch(JsonPatchException): class JsonPatchConflict(JsonPatchException): """Raised if patch could not be applied due to conflict situation such as: - - attempt to add object key then it already exists; + - attempt to add object key when it already exists; - attempt to operate with nonexistence object key; - - attempt to insert value to array at position beyond of it size; + - attempt to insert value to array at position beyond its size; - etc. """ @@ -144,7 +144,7 @@ def apply_patch(doc, patch, in_place=False): def make_patch(src, dst): - """Generates patch by comparing of two document objects. Actually is + """Generates patch by comparing two document objects. Actually is a proxy to :meth:`JsonPatch.from_diff` method. :param src: Data source document object. @@ -181,8 +181,8 @@ class JsonPatch(object): >>> result == expected True - JsonPatch object is iterable, so you could easily access to each patch - statement in loop: + JsonPatch object is iterable, so you can easily access each patch + statement in a loop: >>> lpatch = list(patch) >>> expected = {'op': 'add', 'path': '/foo', 'value': 'bar'} @@ -259,7 +259,7 @@ def from_string(cls, patch_str): @classmethod def from_diff(cls, src, dst, optimization=True): - """Creates JsonPatch instance based on comparing of two document + """Creates JsonPatch instance based on comparison of two document objects. Json patch would be created for `src` argument against `dst` one. @@ -293,13 +293,13 @@ def _ops(self): return tuple(map(self._get_operation, self.patch)) def apply(self, obj, in_place=False): - """Applies the patch to given object. + """Applies the patch to a given object. :param obj: Document object. :type obj: dict - :param in_place: Tweaks way how patch would be applied - directly to - specified `obj` or to his copy. + :param in_place: Tweaks the way how patch would be applied - directly to + specified `obj` or to its copy. :type in_place: bool :return: Modified `obj`. @@ -344,8 +344,8 @@ def __init__(self, operation): self.operation = operation def apply(self, obj): - """Abstract method that applies patch operation to specified object.""" - raise NotImplementedError('should implement patch operation.') + """Abstract method that applies a patch operation to the specified object.""" + raise NotImplementedError('should implement the patch operation.') def __hash__(self): return hash(frozenset(self.operation.items())) @@ -384,7 +384,7 @@ def apply(self, obj): try: del subobj[part] except (KeyError, IndexError) as ex: - msg = "can't remove non-existent object '{0}'".format(part) + msg = "can't remove a non-existent object '{0}'".format(part) raise JsonPatchConflict(msg) return obj @@ -459,7 +459,7 @@ def _on_undo_add(self, path, key): class ReplaceOperation(PatchOperation): - """Replaces an object property or an array element by new value.""" + """Replaces an object property or an array element by a new value.""" def apply(self, obj): try: @@ -479,7 +479,7 @@ def apply(self, obj): elif isinstance(subobj, MutableMapping): if part not in subobj: - msg = "can't replace non-existent object '{0}'".format(part) + msg = "can't replace a non-existent object '{0}'".format(part) raise JsonPatchConflict(msg) else: if part is None: @@ -498,7 +498,7 @@ def _on_undo_add(self, path, key): class MoveOperation(PatchOperation): - """Moves an object property or an array element to new location.""" + """Moves an object property or an array element to a new location.""" def apply(self, obj): try: @@ -522,7 +522,7 @@ def apply(self, obj): if isinstance(subobj, MutableMapping) and \ self.pointer.contains(from_ptr): - raise JsonPatchConflict('Cannot move values into its own children') + raise JsonPatchConflict('Cannot move values into their own children') obj = RemoveOperation({ 'op': 'remove', @@ -826,7 +826,7 @@ def _compare_values(self, path, key, src, dst): self._compare_lists(_path_join(path, key), src, dst) # To ensure we catch changes to JSON, we can't rely on a simple - # src == dst, or it would not recognize the difference between + # src == dst, because it would not recognize the difference between # 1 and True, among other things. Using json.dumps is the most # fool-proof way to ensure we catch type changes that matter to JSON # and ignore those that don't. The performance of this could be From 0167d345ee9d7ef0f74b947ec3a7ea94def178be Mon Sep 17 00:00:00 2001 From: Christian Lyder Jacobsen Date: Fri, 6 Mar 2020 11:41:38 +0100 Subject: [PATCH 03/47] Subclassing can override json dumper and loader Additionally: * from_string gets a loads parameter * to_string gets a dumps_parameter * documentation added * added more tests --- doc/tutorial.rst | 52 ++++++++++++++++++++++++++++++++++++++++++ jsonpatch.py | 22 ++++++++++++------ tests.py | 59 +++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 118 insertions(+), 15 deletions(-) diff --git a/doc/tutorial.rst b/doc/tutorial.rst index 538cd0e..0bb1a9c 100644 --- a/doc/tutorial.rst +++ b/doc/tutorial.rst @@ -67,3 +67,55 @@ explicitly. # or from a list >>> patch = [{'op': 'add', 'path': '/baz', 'value': 'qux'}] >>> res = jsonpatch.apply_patch(obj, patch) + + +Dealing with Custom Types +------------------------- + +Custom JSON dump and load functions can be used to support custom types such as +`decimal.Decimal`. The following examples shows how the +`simplejson `_ package, which has native +support for Python's ``Decimal`` type, can be used to create a custom +``JsonPatch`` subclass with ``Decimal`` support: + +.. code-block:: python + + >>> import decimal + >>> import simplejson + + >>> class DecimalJsonPatch(jsonpatch.JsonPatch): + @staticmethod + def json_dumper(obj): + return simplejson.dumps(obj) + + @staticmethod + def json_loader(obj): + return simplejson.loads(obj, use_decimal=True, + object_pairs_hook=jsonpatch.multidict) + + >>> src = {} + >>> dst = {'bar': decimal.Decimal('1.10')} + >>> patch = DecimalJsonPatch.from_diff(src, dst) + >>> doc = {'foo': 1} + >>> result = patch.apply(doc) + {'foo': 1, 'bar': Decimal('1.10')} + +Instead of subclassing it is also possible to pass a dump function to +``from_diff``: + + >>> patch = jsonpatch.JsonPatch.from_diff(src, dst, dumps=simplejson.dumps) + +a dumps function to ``to_string``: + + >>> serialized_patch = patch.to_string(dumps=simplejson.dumps) + '[{"op": "add", "path": "/bar", "value": 1.10}]' + +and load function to ``from_string``: + + >>> import functools + >>> loads = functools.partial(simplejson.loads, use_decimal=True, + object_pairs_hook=jsonpatch.multidict) + >>> patch.from_string(serialized_patch, loads=loads) + >>> doc = {'foo': 1} + >>> result = patch.apply(doc) + {'foo': 1, 'bar': Decimal('1.10')} diff --git a/jsonpatch.py b/jsonpatch.py index ebaa8e3..ce8a89f 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -165,6 +165,9 @@ def make_patch(src, dst): class JsonPatch(object): + json_dumper = staticmethod(json.dumps) + json_loader = staticmethod(_jsonloads) + """A JSON Patch is a list of Patch Operations. >>> patch = JsonPatch([ @@ -246,19 +249,23 @@ def __ne__(self, other): return not(self == other) @classmethod - def from_string(cls, patch_str): + def from_string(cls, patch_str, loads=None): """Creates JsonPatch instance from string source. :param patch_str: JSON patch as raw string. :type patch_str: str + :param loads: A function of one argument that loads a serialized + JSON string. + :type loads: function :return: :class:`JsonPatch` instance. """ - patch = _jsonloads(patch_str) + json_loader = loads or cls.json_loader + patch = json_loader(patch_str) return cls(patch) @classmethod - def from_diff(cls, src, dst, optimization=True, dumps=json.dumps): + def from_diff(cls, src, dst, optimization=True, dumps=None): """Creates JsonPatch instance based on comparing of two document objects. Json patch would be created for `src` argument against `dst` one. @@ -282,15 +289,16 @@ def from_diff(cls, src, dst, optimization=True, dumps=json.dumps): >>> new == dst True """ - - builder = DiffBuilder(dumps) + json_dumper = dumps or cls.json_dumper + builder = DiffBuilder(json_dumper) builder._compare_values('', None, src, dst) ops = list(builder.execute()) return cls(ops) - def to_string(self): + def to_string(self, dumps=None): """Returns patch set as JSON string.""" - return json.dumps(self.patch) + json_dumper = dumps or self.json_dumper + return json_dumper(self.patch) @property def _ops(self): diff --git a/tests.py b/tests.py index 8837bfa..a843b35 100755 --- a/tests.py +++ b/tests.py @@ -268,6 +268,34 @@ def test_str(self): self.assertEqual(json.dumps(patch_obj), patch.to_string()) +def custom_types_dumps(obj): + def default(obj): + if isinstance(obj, decimal.Decimal): + return {'__decimal__': str(obj)} + raise TypeError('Unknown type') + + return json.dumps(obj, default=default) + + +def custom_types_loads(obj): + def as_decimal(dct): + if '__decimal__' in dct: + return decimal.Decimal(dct['__decimal__']) + return dct + + return json.loads(obj, object_hook=as_decimal) + + +class CustomTypesJsonPatch(jsonpatch.JsonPatch): + @staticmethod + def json_dumper(obj): + return custom_types_dumps(obj) + + @staticmethod + def json_loader(obj): + return custom_types_loads(obj) + + class MakePatchTestCase(unittest.TestCase): def test_apply_patch_to_copy(self): @@ -446,18 +474,33 @@ def test_issue103(self): self.assertEqual(res, dst) self.assertIsInstance(res['A'], float) - def test_custom_types(self): - def default(obj): - if isinstance(obj, decimal.Decimal): - return str(obj) - raise TypeError('Unknown type') + def test_custom_types_diff(self): + old = {'value': decimal.Decimal('1.0')} + new = {'value': decimal.Decimal('1.00')} + generated_patch = jsonpatch.JsonPatch.from_diff( + old, new, dumps=custom_types_dumps) + str_patch = generated_patch.to_string(dumps=custom_types_dumps) + loaded_patch = jsonpatch.JsonPatch.from_string( + str_patch, loads=custom_types_loads) + self.assertEqual(generated_patch, loaded_patch) + new_from_patch = jsonpatch.apply_patch(old, generated_patch) + self.assertEqual(new, new_from_patch) - def dumps(obj): - return json.dumps(obj, default=default) + def test_custom_types_subclass(self): + old = {'value': decimal.Decimal('1.0')} + new = {'value': decimal.Decimal('1.00')} + generated_patch = CustomTypesJsonPatch.from_diff(old, new) + str_patch = generated_patch.to_string() + loaded_patch = CustomTypesJsonPatch.from_string(str_patch) + self.assertEqual(generated_patch, loaded_patch) + new_from_patch = jsonpatch.apply_patch(old, loaded_patch) + self.assertEqual(new, new_from_patch) + def test_custom_types_subclass_load(self): old = {'value': decimal.Decimal('1.0')} new = {'value': decimal.Decimal('1.00')} - patch = jsonpatch.JsonPatch.from_diff(old, new, dumps=dumps) + patch = CustomTypesJsonPatch.from_string( + '[{"op": "replace", "path": "/value", "value": {"__decimal__": "1.00"}}]') new_from_patch = jsonpatch.apply_patch(old, patch) self.assertEqual(new, new_from_patch) From 29c989e815ade4aab25f42047c1ad003358b976d Mon Sep 17 00:00:00 2001 From: Christian Lyder Jacobsen Date: Mon, 16 Mar 2020 21:00:01 +0100 Subject: [PATCH 04/47] Make DiffBuilder's dumps argument optional --- jsonpatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonpatch.py b/jsonpatch.py index 63dcd97..21714c7 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -649,7 +649,7 @@ def apply(self, obj): class DiffBuilder(object): - def __init__(self, dumps): + def __init__(self, dumps=json.dumps): self.dumps = dumps self.index_storage = [{}, {}] self.index_storage2 = [[], []] From 1015d7f35958f196478f32148656171f87358cde Mon Sep 17 00:00:00 2001 From: Alanscut Date: Sat, 23 May 2020 18:16:06 +0800 Subject: [PATCH 05/47] fix #111: optimizing exception message --- jsonpatch.py | 8 +++++++- tests.py | 11 +++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/jsonpatch.py b/jsonpatch.py index ca22e34..22e1156 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -334,12 +334,18 @@ class PatchOperation(object): def __init__(self, operation): + if not operation.__contains__('path'): + raise InvalidJsonPatch("Operation must have a 'path' member") + if isinstance(operation['path'], JsonPointer): self.location = operation['path'].path self.pointer = operation['path'] else: self.location = operation['path'] - self.pointer = JsonPointer(self.location) + try: + self.pointer = JsonPointer(self.location) + except TypeError as ex: + raise InvalidJsonPatch("Invalid 'path'") self.operation = operation diff --git a/tests.py b/tests.py index cde90b0..0abf4d2 100755 --- a/tests.py +++ b/tests.py @@ -219,6 +219,17 @@ def test_append(self): ]) self.assertEqual(res['foo'], [1, 2, 3, 4]) + def test_add_missing_path(self): + obj = {'bar': 'qux'} + self.assertRaises(jsonpatch.InvalidJsonPatch, + jsonpatch.apply_patch, + obj, [{'op': 'test', 'value': 'bar'}]) + + def test_path_with_null_value(self): + obj = {'bar': 'qux'} + self.assertRaises(jsonpatch.InvalidJsonPatch, + jsonpatch.apply_patch, + obj, '[{"op": "add", "path": null, "value": "bar"}]') class EqualityTestCase(unittest.TestCase): From 86f82becdc7f69a1153f2a7400117bed09ebd8c9 Mon Sep 17 00:00:00 2001 From: Alanscut Date: Tue, 9 Jun 2020 20:20:51 +0800 Subject: [PATCH 06/47] fix #102: optimize error handling --- jsonpatch.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/jsonpatch.py b/jsonpatch.py index ca22e34..e042ce2 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -473,6 +473,9 @@ def apply(self, obj): if part is None: return value + if part == "-": + raise InvalidJsonPatch("'path' with '-' can't be applied to 'replace' operation") + if isinstance(subobj, MutableSequence): if part >= len(subobj) or part < 0: raise JsonPatchConflict("can't replace outside of list") From ab775d187539c85cb7214905ad295358b240af14 Mon Sep 17 00:00:00 2001 From: Artyom Nikitin Date: Fri, 13 Nov 2020 00:21:07 +0300 Subject: [PATCH 07/47] feat: add custom json pointer support --- jsonpatch.py | 76 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 32 deletions(-) diff --git a/jsonpatch.py b/jsonpatch.py index 7d5489a..c893bea 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -106,7 +106,7 @@ def multidict(ordered_pairs): _jsonloads = functools.partial(json.loads, object_pairs_hook=multidict) -def apply_patch(doc, patch, in_place=False): +def apply_patch(doc, patch, in_place=False, pointer_cls=JsonPointer): """Apply list of patches to specified json document. :param doc: Document object. @@ -137,13 +137,13 @@ def apply_patch(doc, patch, in_place=False): """ if isinstance(patch, basestring): - patch = JsonPatch.from_string(patch) + patch = JsonPatch.from_string(patch, pointer_cls=pointer_cls) else: - patch = JsonPatch(patch) + patch = JsonPatch(patch, pointer_cls=pointer_cls) return patch.apply(doc, in_place) -def make_patch(src, dst): +def make_patch(src, dst, pointer_cls=JsonPointer): """Generates patch by comparing two document objects. Actually is a proxy to :meth:`JsonPatch.from_diff` method. @@ -153,6 +153,9 @@ def make_patch(src, dst): :param dst: Data source document object. :type dst: dict + :param pointer_cls: JSON pointer (sub)class. + :type pointer_cls: Type[JsonPointer] + >>> src = {'foo': 'bar', 'numbers': [1, 3, 4, 8]} >>> dst = {'baz': 'qux', 'numbers': [1, 4, 7]} >>> patch = make_patch(src, dst) @@ -161,7 +164,7 @@ def make_patch(src, dst): True """ - return JsonPatch.from_diff(src, dst) + return JsonPatch.from_diff(src, dst, pointer_cls=pointer_cls) class JsonPatch(object): @@ -210,8 +213,9 @@ class JsonPatch(object): ... patch.apply(old) #doctest: +ELLIPSIS {...} """ - def __init__(self, patch): + def __init__(self, patch, pointer_cls=JsonPointer): self.patch = patch + self.pointer_cls = pointer_cls self.operations = { 'remove': RemoveOperation, @@ -246,19 +250,22 @@ def __ne__(self, other): return not(self == other) @classmethod - def from_string(cls, patch_str): + def from_string(cls, patch_str, pointer_cls=JsonPointer): """Creates JsonPatch instance from string source. :param patch_str: JSON patch as raw string. - :type patch_str: str + :type pointer_cls: str + + :param pointer_cls: JSON pointer (sub)class. + :type pointer_cls: Type[JsonPointer] :return: :class:`JsonPatch` instance. """ patch = _jsonloads(patch_str) - return cls(patch) + return cls(patch, pointer_cls=pointer_cls) @classmethod - def from_diff(cls, src, dst, optimization=True): + def from_diff(cls, src, dst, optimization=True, pointer_cls=JsonPointer): """Creates JsonPatch instance based on comparison of two document objects. Json patch would be created for `src` argument against `dst` one. @@ -269,6 +276,9 @@ def from_diff(cls, src, dst, optimization=True): :param dst: Data source document object. :type dst: dict + :param pointer_cls: JSON pointer (sub)class. + :type pointer_cls: Type[JsonPointer] + :return: :class:`JsonPatch` instance. >>> src = {'foo': 'bar', 'numbers': [1, 3, 4, 8]} @@ -279,10 +289,10 @@ def from_diff(cls, src, dst, optimization=True): True """ - builder = DiffBuilder() + builder = DiffBuilder(pointer_cls=pointer_cls) builder._compare_values('', None, src, dst) ops = list(builder.execute()) - return cls(ops) + return cls(ops, pointer_cls=pointer_cls) def to_string(self): """Returns patch set as JSON string.""" @@ -326,24 +336,25 @@ def _get_operation(self, operation): raise InvalidJsonPatch("Unknown operation {0!r}".format(op)) cls = self.operations[op] - return cls(operation) + return cls(operation, pointer_cls=self.pointer_cls) class PatchOperation(object): """A single operation inside a JSON Patch.""" - def __init__(self, operation): + def __init__(self, operation, pointer_cls=JsonPointer): + self.pointer_cls = pointer_cls if not operation.__contains__('path'): raise InvalidJsonPatch("Operation must have a 'path' member") - if isinstance(operation['path'], JsonPointer): + if isinstance(operation['path'], self.pointer_cls): self.location = operation['path'].path self.pointer = operation['path'] else: self.location = operation['path'] try: - self.pointer = JsonPointer(self.location) + self.pointer = self.pointer_cls(self.location) except TypeError as ex: raise InvalidJsonPatch("Invalid 'path'") @@ -511,10 +522,10 @@ class MoveOperation(PatchOperation): def apply(self, obj): try: - if isinstance(self.operation['from'], JsonPointer): + if isinstance(self.operation['from'], self.pointer_cls): from_ptr = self.operation['from'] else: - from_ptr = JsonPointer(self.operation['from']) + from_ptr = self.pointer_cls(self.operation['from']) except KeyError as ex: raise InvalidJsonPatch( "The operation does not contain a 'from' member") @@ -536,24 +547,24 @@ def apply(self, obj): obj = RemoveOperation({ 'op': 'remove', 'path': self.operation['from'] - }).apply(obj) + }, pointer_cls=self.pointer_cls).apply(obj) obj = AddOperation({ 'op': 'add', 'path': self.location, 'value': value - }).apply(obj) + }, pointer_cls=self.pointer_cls).apply(obj) return obj @property def from_path(self): - from_ptr = JsonPointer(self.operation['from']) + from_ptr = self.pointer_cls(self.operation['from']) return '/'.join(from_ptr.parts[:-1]) @property def from_key(self): - from_ptr = JsonPointer(self.operation['from']) + from_ptr = self.pointer_cls(self.operation['from']) try: return int(from_ptr.parts[-1]) except TypeError: @@ -561,7 +572,7 @@ def from_key(self): @from_key.setter def from_key(self, value): - from_ptr = JsonPointer(self.operation['from']) + from_ptr = self.pointer_cls(self.operation['from']) from_ptr.parts[-1] = str(value) self.operation['from'] = from_ptr.path @@ -624,7 +635,7 @@ class CopyOperation(PatchOperation): def apply(self, obj): try: - from_ptr = JsonPointer(self.operation['from']) + from_ptr = self.pointer_cls(self.operation['from']) except KeyError as ex: raise InvalidJsonPatch( "The operation does not contain a 'from' member") @@ -639,14 +650,15 @@ def apply(self, obj): 'op': 'add', 'path': self.location, 'value': value - }).apply(obj) + }, pointer_cls=self.pointer_cls).apply(obj) return obj class DiffBuilder(object): - def __init__(self): + def __init__(self, pointer_cls=JsonPointer): + self.pointer_cls = pointer_cls self.index_storage = [{}, {}] self.index_storage2 = [[], []] self.__root = root = [] @@ -715,7 +727,7 @@ def execute(self): 'op': 'replace', 'path': op_second.location, 'value': op_second.operation['value'], - }).operation + }, pointer_cls=self.pointer_cls).operation curr = curr[1][1] continue @@ -736,14 +748,14 @@ def _item_added(self, path, key, item): 'op': 'move', 'from': op.location, 'path': _path_join(path, key), - }) + }, pointer_cls=self.pointer_cls) self.insert(new_op) else: new_op = AddOperation({ 'op': 'add', 'path': _path_join(path, key), 'value': item, - }) + }, pointer_cls=self.pointer_cls) new_index = self.insert(new_op) self.store_index(item, new_index, _ST_ADD) @@ -751,7 +763,7 @@ def _item_removed(self, path, key, item): new_op = RemoveOperation({ 'op': 'remove', 'path': _path_join(path, key), - }) + }, pointer_cls=self.pointer_cls) index = self.take_index(item, _ST_ADD) new_index = self.insert(new_op) if index is not None: @@ -766,7 +778,7 @@ def _item_removed(self, path, key, item): 'op': 'move', 'from': new_op.location, 'path': op.location, - }) + }, pointer_cls=self.pointer_cls) new_index[2] = new_op else: @@ -780,7 +792,7 @@ def _item_replaced(self, path, key, item): 'op': 'replace', 'path': _path_join(path, key), 'value': item, - })) + }, pointer_cls=self.pointer_cls)) def _compare_dicts(self, path, src, dst): src_keys = set(src.keys()) From bb4ea7ba669b26d29f31ec75015d92fb6633f07b Mon Sep 17 00:00:00 2001 From: Artyom Nikitin Date: Fri, 13 Nov 2020 00:21:16 +0300 Subject: [PATCH 08/47] test: custo json pointer --- tests.py | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests.py b/tests.py index 0abf4d2..0402a03 100755 --- a/tests.py +++ b/tests.py @@ -671,6 +671,59 @@ def test_create_with_pointer(self): self.assertEqual(result, expected) +class CustomJsonPointerTests(unittest.TestCase): + + class CustomJsonPointer(jsonpointer.JsonPointer): + pass + + def test_apply_patch_from_string(self): + obj = {'foo': 'bar'} + patch = '[{"op": "add", "path": "/baz", "value": "qux"}]' + res = jsonpatch.apply_patch( + obj, patch, + pointer_cls=self.CustomJsonPointer, + ) + self.assertTrue(obj is not res) + self.assertTrue('baz' in res) + self.assertEqual(res['baz'], 'qux') + + def test_apply_patch_from_object(self): + obj = {'foo': 'bar'} + res = jsonpatch.apply_patch( + obj, [{'op': 'add', 'path': '/baz', 'value': 'qux'}], + pointer_cls=self.CustomJsonPointer, + ) + self.assertTrue(obj is not res) + + def test_make_patch(self): + src = {'foo': 'bar', 'boo': 'qux'} + dst = {'baz': 'qux', 'foo': 'boo'} + patch = jsonpatch.make_patch( + src, dst, pointer_cls=self.CustomJsonPointer, + ) + res = patch.apply(src) + self.assertTrue(src is not res) + self.assertEqual(patch.pointer_cls, self.CustomJsonPointer) + self.assertTrue(patch._ops) + for op in patch._ops: + self.assertEqual(op.pointer_cls, self.CustomJsonPointer) + + def test_operations(self): + patch = jsonpatch.JsonPatch([ + {'op': 'add', 'path': '/foo', 'value': [1, 2, 3]}, + {'op': 'move', 'path': '/baz', 'from': '/foo'}, + {'op': 'add', 'path': '/baz', 'value': [1, 2, 3]}, + {'op': 'remove', 'path': '/baz/1'}, + {'op': 'test', 'path': '/baz', 'value': [1, 3]}, + {'op': 'replace', 'path': '/baz/0', 'value': 42}, + {'op': 'remove', 'path': '/baz/1'}, + ], pointer_cls=self.CustomJsonPointer) + self.assertEqual(patch.apply({}), {'baz': [42]}) + self.assertEqual(patch.pointer_cls, self.CustomJsonPointer) + self.assertTrue(patch._ops) + for op in patch._ops: + self.assertEqual(op.pointer_cls, self.CustomJsonPointer) + if __name__ == '__main__': modules = ['jsonpatch'] @@ -687,6 +740,7 @@ def get_suite(): suite.addTest(unittest.makeSuite(ConflictTests)) suite.addTest(unittest.makeSuite(OptimizationTests)) suite.addTest(unittest.makeSuite(JsonPointerTests)) + suite.addTest(unittest.makeSuite(CustomJsonPointerTests)) return suite From 124eb76c09136aef56618e7347230f981edd51c3 Mon Sep 17 00:00:00 2001 From: Artyom Nikitin Date: Fri, 13 Nov 2020 00:27:30 +0300 Subject: [PATCH 09/47] doc: fix docstrings --- jsonpatch.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/jsonpatch.py b/jsonpatch.py index c893bea..92857ef 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -119,6 +119,9 @@ def apply_patch(doc, patch, in_place=False, pointer_cls=JsonPointer): By default patch will be applied to document copy. :type in_place: bool + :param pointer_cls: JSON pointer class to use. + :type pointer_cls: Type[JsonPointer] + :return: Patched document object. :rtype: dict @@ -153,7 +156,7 @@ def make_patch(src, dst, pointer_cls=JsonPointer): :param dst: Data source document object. :type dst: dict - :param pointer_cls: JSON pointer (sub)class. + :param pointer_cls: JSON pointer class to use. :type pointer_cls: Type[JsonPointer] >>> src = {'foo': 'bar', 'numbers': [1, 3, 4, 8]} @@ -256,7 +259,7 @@ def from_string(cls, patch_str, pointer_cls=JsonPointer): :param patch_str: JSON patch as raw string. :type pointer_cls: str - :param pointer_cls: JSON pointer (sub)class. + :param pointer_cls: JSON pointer class to use. :type pointer_cls: Type[JsonPointer] :return: :class:`JsonPatch` instance. @@ -276,7 +279,7 @@ def from_diff(cls, src, dst, optimization=True, pointer_cls=JsonPointer): :param dst: Data source document object. :type dst: dict - :param pointer_cls: JSON pointer (sub)class. + :param pointer_cls: JSON pointer class to use. :type pointer_cls: Type[JsonPointer] :return: :class:`JsonPatch` instance. From fb04fcc4df0e060586f6401b61af703d60bb6b65 Mon Sep 17 00:00:00 2001 From: Artyom Nikitin Date: Sun, 15 Nov 2020 17:27:33 +0300 Subject: [PATCH 10/47] test: add more tests --- tests.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests.py b/tests.py index 0402a03..ec4758d 100755 --- a/tests.py +++ b/tests.py @@ -676,6 +676,28 @@ class CustomJsonPointerTests(unittest.TestCase): class CustomJsonPointer(jsonpointer.JsonPointer): pass + def test_json_patch_from_string(self): + patch = '[{"op": "add", "path": "/baz", "value": "qux"}]' + res = jsonpatch.JsonPatch.from_string( + patch, pointer_cls=self.CustomJsonPointer, + ) + self.assertEqual(res.pointer_cls, self.CustomJsonPointer) + + def test_json_patch_from_object(self): + patch = [{'op': 'add', 'path': '/baz', 'value': 'qux'}], + res = jsonpatch.JsonPatch( + patch, pointer_cls=self.CustomJsonPointer, + ) + self.assertEqual(res.pointer_cls, self.CustomJsonPointer) + + def test_json_patch_from_diff(self): + old = {'foo': 'bar'} + new = {'foo': 'baz'} + res = jsonpatch.JsonPatch.from_diff( + old, new, pointer_cls=self.CustomJsonPointer, + ) + self.assertEqual(res.pointer_cls, self.CustomJsonPointer) + def test_apply_patch_from_string(self): obj = {'foo': 'bar'} patch = '[{"op": "add", "path": "/baz", "value": "qux"}]' From 0b680ea87afc6e747fc584aaef513815de0c52c3 Mon Sep 17 00:00:00 2001 From: Artyom Nikitin Date: Mon, 16 Nov 2020 10:53:52 +0300 Subject: [PATCH 11/47] chore: bump version --- jsonpatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonpatch.py b/jsonpatch.py index 92857ef..201e9d1 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -56,7 +56,7 @@ # Will be parsed by setup.py to determine package metadata __author__ = 'Stefan Kögl ' -__version__ = '1.24' +__version__ = '1.27' __website__ = 'https://github.com/stefankoegl/python-json-patch' __license__ = 'Modified BSD License' From c37b40ffec5674bf76bbb2197917e528e74b4552 Mon Sep 17 00:00:00 2001 From: Artyom Nikitin Date: Mon, 16 Nov 2020 16:05:00 +0300 Subject: [PATCH 12/47] test: update --- tests.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests.py b/tests.py index ec4758d..941c685 100755 --- a/tests.py +++ b/tests.py @@ -728,6 +728,7 @@ def test_make_patch(self): self.assertEqual(patch.pointer_cls, self.CustomJsonPointer) self.assertTrue(patch._ops) for op in patch._ops: + self.assertIsInstance(op.pointer, self.CustomJsonPointer) self.assertEqual(op.pointer_cls, self.CustomJsonPointer) def test_operations(self): @@ -744,6 +745,7 @@ def test_operations(self): self.assertEqual(patch.pointer_cls, self.CustomJsonPointer) self.assertTrue(patch._ops) for op in patch._ops: + self.assertIsInstance(op.pointer, self.CustomJsonPointer) self.assertEqual(op.pointer_cls, self.CustomJsonPointer) From 0994bfe2ce199d6edccb4ab97fc10e3c26683348 Mon Sep 17 00:00:00 2001 From: Artyom Nikitin Date: Tue, 17 Nov 2020 11:02:15 +0300 Subject: [PATCH 13/47] test: add toy jsonpointer example --- tests.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests.py b/tests.py index 941c685..25c9e4f 100755 --- a/tests.py +++ b/tests.py @@ -748,6 +748,17 @@ def test_operations(self): self.assertIsInstance(op.pointer, self.CustomJsonPointer) self.assertEqual(op.pointer_cls, self.CustomJsonPointer) + class PrefixJsonPointer(jsonpointer.JsonPointer): + def __init__(self, pointer): + super().__init__('/foo/bar' + pointer) + + def test_json_patch_wtih_prefix_pointer(self): + res = jsonpatch.apply_patch( + {'foo': {'bar': {}}}, [{'op': 'add', 'path': '/baz', 'value': 'qux'}], + pointer_cls=self.PrefixJsonPointer, + ) + self.assertEqual(res, {'foo': {'bar': {'baz': 'qux'}}}) + if __name__ == '__main__': modules = ['jsonpatch'] From d24fa96a7ad1f01cb793c0efe835a76ddd3b2fc7 Mon Sep 17 00:00:00 2001 From: Artyom Nikitin Date: Tue, 17 Nov 2020 11:28:45 +0300 Subject: [PATCH 14/47] test: fix for py27 --- tests.py | 50 ++++++++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/tests.py b/tests.py index 25c9e4f..2dfc18c 100755 --- a/tests.py +++ b/tests.py @@ -671,39 +671,45 @@ def test_create_with_pointer(self): self.assertEqual(result, expected) -class CustomJsonPointerTests(unittest.TestCase): +class CustomJsonPointer(jsonpointer.JsonPointer): + pass + + +class PrefixJsonPointer(jsonpointer.JsonPointer): + def __init__(self, pointer): + super(PrefixJsonPointer, self).__init__('/foo/bar' + pointer) - class CustomJsonPointer(jsonpointer.JsonPointer): - pass + +class CustomJsonPointerTests(unittest.TestCase): def test_json_patch_from_string(self): patch = '[{"op": "add", "path": "/baz", "value": "qux"}]' res = jsonpatch.JsonPatch.from_string( - patch, pointer_cls=self.CustomJsonPointer, + patch, pointer_cls=CustomJsonPointer, ) - self.assertEqual(res.pointer_cls, self.CustomJsonPointer) + self.assertEqual(res.pointer_cls, CustomJsonPointer) def test_json_patch_from_object(self): patch = [{'op': 'add', 'path': '/baz', 'value': 'qux'}], res = jsonpatch.JsonPatch( - patch, pointer_cls=self.CustomJsonPointer, + patch, pointer_cls=CustomJsonPointer, ) - self.assertEqual(res.pointer_cls, self.CustomJsonPointer) + self.assertEqual(res.pointer_cls, CustomJsonPointer) def test_json_patch_from_diff(self): old = {'foo': 'bar'} new = {'foo': 'baz'} res = jsonpatch.JsonPatch.from_diff( - old, new, pointer_cls=self.CustomJsonPointer, + old, new, pointer_cls=CustomJsonPointer, ) - self.assertEqual(res.pointer_cls, self.CustomJsonPointer) + self.assertEqual(res.pointer_cls, CustomJsonPointer) def test_apply_patch_from_string(self): obj = {'foo': 'bar'} patch = '[{"op": "add", "path": "/baz", "value": "qux"}]' res = jsonpatch.apply_patch( obj, patch, - pointer_cls=self.CustomJsonPointer, + pointer_cls=CustomJsonPointer, ) self.assertTrue(obj is not res) self.assertTrue('baz' in res) @@ -713,7 +719,7 @@ def test_apply_patch_from_object(self): obj = {'foo': 'bar'} res = jsonpatch.apply_patch( obj, [{'op': 'add', 'path': '/baz', 'value': 'qux'}], - pointer_cls=self.CustomJsonPointer, + pointer_cls=CustomJsonPointer, ) self.assertTrue(obj is not res) @@ -721,15 +727,15 @@ def test_make_patch(self): src = {'foo': 'bar', 'boo': 'qux'} dst = {'baz': 'qux', 'foo': 'boo'} patch = jsonpatch.make_patch( - src, dst, pointer_cls=self.CustomJsonPointer, + src, dst, pointer_cls=CustomJsonPointer, ) res = patch.apply(src) self.assertTrue(src is not res) - self.assertEqual(patch.pointer_cls, self.CustomJsonPointer) + self.assertEqual(patch.pointer_cls, CustomJsonPointer) self.assertTrue(patch._ops) for op in patch._ops: - self.assertIsInstance(op.pointer, self.CustomJsonPointer) - self.assertEqual(op.pointer_cls, self.CustomJsonPointer) + self.assertIsInstance(op.pointer, CustomJsonPointer) + self.assertEqual(op.pointer_cls, CustomJsonPointer) def test_operations(self): patch = jsonpatch.JsonPatch([ @@ -740,22 +746,18 @@ def test_operations(self): {'op': 'test', 'path': '/baz', 'value': [1, 3]}, {'op': 'replace', 'path': '/baz/0', 'value': 42}, {'op': 'remove', 'path': '/baz/1'}, - ], pointer_cls=self.CustomJsonPointer) + ], pointer_cls=CustomJsonPointer) self.assertEqual(patch.apply({}), {'baz': [42]}) - self.assertEqual(patch.pointer_cls, self.CustomJsonPointer) + self.assertEqual(patch.pointer_cls, CustomJsonPointer) self.assertTrue(patch._ops) for op in patch._ops: - self.assertIsInstance(op.pointer, self.CustomJsonPointer) - self.assertEqual(op.pointer_cls, self.CustomJsonPointer) - - class PrefixJsonPointer(jsonpointer.JsonPointer): - def __init__(self, pointer): - super().__init__('/foo/bar' + pointer) + self.assertIsInstance(op.pointer, CustomJsonPointer) + self.assertEqual(op.pointer_cls, CustomJsonPointer) def test_json_patch_wtih_prefix_pointer(self): res = jsonpatch.apply_patch( {'foo': {'bar': {}}}, [{'op': 'add', 'path': '/baz', 'value': 'qux'}], - pointer_cls=self.PrefixJsonPointer, + pointer_cls=PrefixJsonPointer, ) self.assertEqual(res, {'foo': {'bar': {'baz': 'qux'}}}) From 4d073929b732af3403ae9fac92433e0066f0061a Mon Sep 17 00:00:00 2001 From: Artyom Nikitin Date: Tue, 17 Nov 2020 11:29:23 +0300 Subject: [PATCH 15/47] style: fix typo --- tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests.py b/tests.py index 2dfc18c..c676121 100755 --- a/tests.py +++ b/tests.py @@ -754,7 +754,7 @@ def test_operations(self): self.assertIsInstance(op.pointer, CustomJsonPointer) self.assertEqual(op.pointer_cls, CustomJsonPointer) - def test_json_patch_wtih_prefix_pointer(self): + def test_json_patch_with_prefix_pointer(self): res = jsonpatch.apply_patch( {'foo': {'bar': {}}}, [{'op': 'add', 'path': '/baz', 'value': 'qux'}], pointer_cls=PrefixJsonPointer, From c9613e303531ce4a016b3a696992743e62e12258 Mon Sep 17 00:00:00 2001 From: Artyom Nikitin Date: Tue, 17 Nov 2020 11:52:33 +0300 Subject: [PATCH 16/47] chore: revert version bump --- jsonpatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonpatch.py b/jsonpatch.py index 201e9d1..92857ef 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -56,7 +56,7 @@ # Will be parsed by setup.py to determine package metadata __author__ = 'Stefan Kögl ' -__version__ = '1.27' +__version__ = '1.24' __website__ = 'https://github.com/stefankoegl/python-json-patch' __license__ = 'Modified BSD License' From bfc0f5a68fc45a1335488c953fd055750528f16e Mon Sep 17 00:00:00 2001 From: Dave Shawley Date: Tue, 17 Nov 2020 08:13:36 -0500 Subject: [PATCH 17/47] Update coveragerc and require coverage. --- .coveragerc | 2 ++ requirements-dev.txt | 1 + 2 files changed, 3 insertions(+) diff --git a/.coveragerc b/.coveragerc index 2a98e09..f0d91db 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,8 +1,10 @@ # .coveragerc to control coverage.py [run] branch = True +source = jsonpatch [report] +show_missing = True # Regexes for lines to exclude from consideration exclude_lines = # Have to re-enable the standard pragma diff --git a/requirements-dev.txt b/requirements-dev.txt index 21daf9a..c729ece 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,2 +1,3 @@ +coverage wheel pypandoc From a7ef7e80d0024b71794c22fd09e35389c04de964 Mon Sep 17 00:00:00 2001 From: Dave Shawley Date: Tue, 17 Nov 2020 08:15:25 -0500 Subject: [PATCH 18/47] fix #110: Validate patch documents during creation. --- jsonpatch.py | 3 +++ tests.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/jsonpatch.py b/jsonpatch.py index 7d5489a..022972b 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -222,6 +222,9 @@ def __init__(self, patch): 'copy': CopyOperation, } + for op in self.patch: + self._get_operation(op) + def __str__(self): """str(self) -> self.to_string()""" return self.to_string() diff --git a/tests.py b/tests.py index 0abf4d2..a7c1a43 100755 --- a/tests.py +++ b/tests.py @@ -671,6 +671,22 @@ def test_create_with_pointer(self): self.assertEqual(result, expected) +class JsonPatchCreationTest(unittest.TestCase): + + def test_creation_fails_with_invalid_patch(self): + invalid_patches = [ + { 'path': '/foo', 'value': 'bar'}, + {'op': 0xADD, 'path': '/foo', 'value': 'bar'}, + {'op': 'boo', 'path': '/foo', 'value': 'bar'}, + {'op': 'add', 'value': 'bar'}, + ] + for patch in invalid_patches: + with self.assertRaises(jsonpatch.InvalidJsonPatch): + jsonpatch.JsonPatch([patch]) + + with self.assertRaises(jsonpointer.JsonPointerException): + jsonpatch.JsonPatch([{'op': 'add', 'path': 'foo', 'value': 'bar'}]) + if __name__ == '__main__': modules = ['jsonpatch'] @@ -687,6 +703,7 @@ def get_suite(): suite.addTest(unittest.makeSuite(ConflictTests)) suite.addTest(unittest.makeSuite(OptimizationTests)) suite.addTest(unittest.makeSuite(JsonPointerTests)) + suite.addTest(unittest.makeSuite(JsonPatchCreationTest)) return suite From b44e7a2031ad5cbe0a0d5ad2ab0763b7b9b8dc25 Mon Sep 17 00:00:00 2001 From: Dave Shawley Date: Tue, 17 Nov 2020 08:17:07 -0500 Subject: [PATCH 19/47] Add tests for operation doc structure. --- tests.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tests.py b/tests.py index a7c1a43..58e3ce2 100755 --- a/tests.py +++ b/tests.py @@ -688,6 +688,69 @@ def test_creation_fails_with_invalid_patch(self): jsonpatch.JsonPatch([{'op': 'add', 'path': 'foo', 'value': 'bar'}]) +class UtilityMethodTests(unittest.TestCase): + + def test_boolean_coercion(self): + empty_patch = jsonpatch.JsonPatch([]) + self.assertFalse(empty_patch) + + def test_patch_equality(self): + p = jsonpatch.JsonPatch([{'op': 'add', 'path': '/foo', 'value': 'bar'}]) + q = jsonpatch.JsonPatch([{'op': 'add', 'path': '/foo', 'value': 'bar'}]) + different_op = jsonpatch.JsonPatch([{'op': 'remove', 'path': '/foo'}]) + different_path = jsonpatch.JsonPatch([{'op': 'add', 'path': '/bar', 'value': 'bar'}]) + different_value = jsonpatch.JsonPatch([{'op': 'add', 'path': '/foo', 'value': 'foo'}]) + self.assertNotEqual(p, different_op) + self.assertNotEqual(p, different_path) + self.assertNotEqual(p, different_value) + self.assertEqual(p, q) + + def test_operation_equality(self): + add = jsonpatch.AddOperation({'path': '/new-element', 'value': 'new-value'}) + add2 = jsonpatch.AddOperation({'path': '/new-element', 'value': 'new-value'}) + rm = jsonpatch.RemoveOperation({'path': '/target'}) + self.assertEqual(add, add2) + self.assertNotEqual(add, rm) + + def test_add_operation_structure(self): + with self.assertRaises(jsonpatch.InvalidJsonPatch): + jsonpatch.AddOperation({'path': '/'}).apply({}) + + def test_replace_operation_structure(self): + with self.assertRaises(jsonpatch.InvalidJsonPatch): + jsonpatch.ReplaceOperation({'path': '/'}).apply({}) + + with self.assertRaises(jsonpatch.InvalidJsonPatch): + jsonpatch.ReplaceOperation({'path': '/top/-', 'value': 'foo'}).apply({'top': {'inner': 'value'}}) + + with self.assertRaises(jsonpatch.JsonPatchConflict): + jsonpatch.ReplaceOperation({'path': '/top/missing', 'value': 'foo'}).apply({'top': {'inner': 'value'}}) + + def test_move_operation_structure(self): + with self.assertRaises(jsonpatch.InvalidJsonPatch): + jsonpatch.MoveOperation({'path': '/target'}).apply({}) + + with self.assertRaises(jsonpatch.JsonPatchConflict): + jsonpatch.MoveOperation({'from': '/source', 'path': '/target'}).apply({}) + + def test_test_operation_structure(self): + with self.assertRaises(jsonpatch.JsonPatchTestFailed): + jsonpatch.TestOperation({'path': '/target'}).apply({}) + + with self.assertRaises(jsonpatch.InvalidJsonPatch): + jsonpatch.TestOperation({'path': '/target'}).apply({'target': 'value'}) + + def test_copy_operation_structure(self): + with self.assertRaises(jsonpatch.InvalidJsonPatch): + jsonpatch.CopyOperation({'path': '/target'}).apply({}) + + with self.assertRaises(jsonpatch.JsonPatchConflict): + jsonpatch.CopyOperation({'path': '/target', 'from': '/source'}).apply({}) + + with self.assertRaises(jsonpatch.JsonPatchConflict): + jsonpatch.CopyOperation({'path': '/target', 'from': '/source'}).apply({}) + + if __name__ == '__main__': modules = ['jsonpatch'] @@ -704,6 +767,7 @@ def get_suite(): suite.addTest(unittest.makeSuite(OptimizationTests)) suite.addTest(unittest.makeSuite(JsonPointerTests)) suite.addTest(unittest.makeSuite(JsonPatchCreationTest)) + suite.addTest(unittest.makeSuite(UtilityMethodTests)) return suite From 50fb942e3500d84950ec9309f886f1952bd2fa25 Mon Sep 17 00:00:00 2001 From: Artyom Nikitin Date: Tue, 17 Nov 2020 20:54:36 +0300 Subject: [PATCH 20/47] tests: moar --- tests.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests.py b/tests.py index c676121..7df2c2b 100755 --- a/tests.py +++ b/tests.py @@ -738,6 +738,44 @@ def test_make_patch(self): self.assertEqual(op.pointer_cls, CustomJsonPointer) def test_operations(self): + operations =[ + ( + jsonpatch.AddOperation, { + 'op': 'add', 'path': '/foo', 'value': [1, 2, 3] + } + ), + ( + jsonpatch.MoveOperation, { + 'op': 'move', 'path': '/baz', 'from': '/foo' + }, + ), + ( + jsonpatch.RemoveOperation, { + 'op': 'remove', 'path': '/baz/1' + }, + ), + ( + jsonpatch.TestOperation, { + 'op': 'test', 'path': '/baz', 'value': [1, 3] + }, + ), + ( + jsonpatch.ReplaceOperation, { + 'op': 'replace', 'path': '/baz/0', 'value': 42 + }, + ), + ( + jsonpatch.RemoveOperation, { + 'op': 'remove', 'path': '/baz/1' + }, + ) + ] + for cls, patch in operations: + operation = cls(patch, pointer_cls=CustomJsonPointer) + self.assertEqual(operation.pointer_cls, CustomJsonPointer) + self.assertIsInstance(operation.pointer, CustomJsonPointer) + + def test_operations_from_patch(self): patch = jsonpatch.JsonPatch([ {'op': 'add', 'path': '/foo', 'value': [1, 2, 3]}, {'op': 'move', 'path': '/baz', 'from': '/foo'}, From 3bb33518194b0cbc6e1512dbeb2ac5ef548d8c72 Mon Sep 17 00:00:00 2001 From: Dave Shawley Date: Fri, 20 Nov 2020 07:22:03 -0500 Subject: [PATCH 21/47] Explain the call to _get_operation in __init__. --- jsonpatch.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/jsonpatch.py b/jsonpatch.py index 022972b..2b8678f 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -222,6 +222,10 @@ def __init__(self, patch): 'copy': CopyOperation, } + # Verify that the structure of the patch document + # is correct by retrieving each patch element. + # Much of the validation is done in the initializer + # though some is delayed until the patch is applied. for op in self.patch: self._get_operation(op) From 3972a8e648b7d761b92ee53591fc24b9d805a90d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Mon, 23 Nov 2020 19:44:43 +0000 Subject: [PATCH 22/47] Update Python to 3.9 --- .travis.yml | 3 ++- setup.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6a02844..48f6882 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,8 @@ python: - '3.6' - '3.7' - '3.8' -- 3.8-dev +- '3.9' +- 3.10-dev - nightly - pypy - pypy3 diff --git a/setup.py b/setup.py index b01af80..ad43bd5 100644 --- a/setup.py +++ b/setup.py @@ -60,6 +60,7 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Libraries', From b8083d703c3aacf52429a06dc5b482a1f9acf54f Mon Sep 17 00:00:00 2001 From: Artyom Nikitin Date: Mon, 23 Nov 2020 23:52:42 +0300 Subject: [PATCH 23/47] feat: make operations class-based --- jsonpatch.py | 395 ++++++++++++++++++++++++++------------------------- 1 file changed, 200 insertions(+), 195 deletions(-) diff --git a/jsonpatch.py b/jsonpatch.py index 5522d50..a01a177 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -39,6 +39,11 @@ import functools import json import sys +try: + from types import MappingProxyType +except ImportError: + # Python < 3.3 + MappingProxyType = dict from jsonpointer import JsonPointer, JsonPointerException @@ -170,201 +175,6 @@ def make_patch(src, dst, pointer_cls=JsonPointer): return JsonPatch.from_diff(src, dst, pointer_cls=pointer_cls) -class JsonPatch(object): - json_dumper = staticmethod(json.dumps) - json_loader = staticmethod(_jsonloads) - - """A JSON Patch is a list of Patch Operations. - - >>> patch = JsonPatch([ - ... {'op': 'add', 'path': '/foo', 'value': 'bar'}, - ... {'op': 'add', 'path': '/baz', 'value': [1, 2, 3]}, - ... {'op': 'remove', 'path': '/baz/1'}, - ... {'op': 'test', 'path': '/baz', 'value': [1, 3]}, - ... {'op': 'replace', 'path': '/baz/0', 'value': 42}, - ... {'op': 'remove', 'path': '/baz/1'}, - ... ]) - >>> doc = {} - >>> result = patch.apply(doc) - >>> expected = {'foo': 'bar', 'baz': [42]} - >>> result == expected - True - - JsonPatch object is iterable, so you can easily access each patch - statement in a loop: - - >>> lpatch = list(patch) - >>> expected = {'op': 'add', 'path': '/foo', 'value': 'bar'} - >>> lpatch[0] == expected - True - >>> lpatch == patch.patch - True - - Also JsonPatch could be converted directly to :class:`bool` if it contains - any operation statements: - - >>> bool(patch) - True - >>> bool(JsonPatch([])) - False - - This behavior is very handy with :func:`make_patch` to write more readable - code: - - >>> old = {'foo': 'bar', 'numbers': [1, 3, 4, 8]} - >>> new = {'baz': 'qux', 'numbers': [1, 4, 7]} - >>> patch = make_patch(old, new) - >>> if patch: - ... # document have changed, do something useful - ... patch.apply(old) #doctest: +ELLIPSIS - {...} - """ - def __init__(self, patch, pointer_cls=JsonPointer): - self.patch = patch - self.pointer_cls = pointer_cls - - self.operations = { - 'remove': RemoveOperation, - 'add': AddOperation, - 'replace': ReplaceOperation, - 'move': MoveOperation, - 'test': TestOperation, - 'copy': CopyOperation, - } - - # Verify that the structure of the patch document - # is correct by retrieving each patch element. - # Much of the validation is done in the initializer - # though some is delayed until the patch is applied. - for op in self.patch: - self._get_operation(op) - - def __str__(self): - """str(self) -> self.to_string()""" - return self.to_string() - - def __bool__(self): - return bool(self.patch) - - __nonzero__ = __bool__ - - 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 - - def __ne__(self, other): - return not(self == other) - - @classmethod - def from_string(cls, patch_str, loads=None, pointer_cls=JsonPointer): - """Creates JsonPatch instance from string source. - - :param patch_str: JSON patch as raw string. - :type patch_str: str - - :param loads: A function of one argument that loads a serialized - JSON string. - :type loads: function - - :param pointer_cls: JSON pointer class to use. - :type pointer_cls: Type[JsonPointer] - - :return: :class:`JsonPatch` instance. - """ - json_loader = loads or cls.json_loader - patch = json_loader(patch_str) - return cls(patch, pointer_cls=pointer_cls) - - @classmethod - def from_diff( - cls, src, dst, optimization=True, dumps=None, - pointer_cls=JsonPointer, - ): - """Creates JsonPatch instance based on comparison of two document - objects. Json patch would be created for `src` argument against `dst` - one. - - :param src: Data source document object. - :type src: dict - - :param dst: Data source document object. - :type dst: dict - - :param dumps: A function of one argument that produces a serialized - JSON string. - :type dumps: function - - :param pointer_cls: JSON pointer class to use. - :type pointer_cls: Type[JsonPointer] - - :return: :class:`JsonPatch` instance. - - >>> src = {'foo': 'bar', 'numbers': [1, 3, 4, 8]} - >>> dst = {'baz': 'qux', 'numbers': [1, 4, 7]} - >>> patch = JsonPatch.from_diff(src, dst) - >>> new = patch.apply(src) - >>> new == dst - True - """ - json_dumper = dumps or cls.json_dumper - builder = DiffBuilder(json_dumper, pointer_cls=pointer_cls) - builder._compare_values('', None, src, dst) - ops = list(builder.execute()) - return cls(ops, pointer_cls=pointer_cls) - - def to_string(self, dumps=None): - """Returns patch set as JSON string.""" - json_dumper = dumps or self.json_dumper - return json_dumper(self.patch) - - @property - def _ops(self): - return tuple(map(self._get_operation, self.patch)) - - def apply(self, obj, in_place=False): - """Applies the patch to a given object. - - :param obj: Document object. - :type obj: dict - - :param in_place: Tweaks the way how patch would be applied - directly to - specified `obj` or to its copy. - :type in_place: bool - - :return: Modified `obj`. - """ - - if not in_place: - obj = copy.deepcopy(obj) - - for operation in self._ops: - obj = operation.apply(obj) - - return obj - - def _get_operation(self, operation): - if 'op' not in operation: - raise InvalidJsonPatch("Operation does not contain 'op' member") - - op = operation['op'] - - if not isinstance(op, basestring): - raise InvalidJsonPatch("Operation must be a string") - - if op not in self.operations: - raise InvalidJsonPatch("Unknown operation {0!r}".format(op)) - - cls = self.operations[op] - return cls(operation, pointer_cls=self.pointer_cls) - - class PatchOperation(object): """A single operation inside a JSON Patch.""" @@ -681,6 +491,201 @@ def apply(self, obj): return obj +class JsonPatch(object): + json_dumper = staticmethod(json.dumps) + json_loader = staticmethod(_jsonloads) + + operations = MappingProxyType({ + 'remove': RemoveOperation, + 'add': AddOperation, + 'replace': ReplaceOperation, + 'move': MoveOperation, + 'test': TestOperation, + 'copy': CopyOperation, + }) + + """A JSON Patch is a list of Patch Operations. + + >>> patch = JsonPatch([ + ... {'op': 'add', 'path': '/foo', 'value': 'bar'}, + ... {'op': 'add', 'path': '/baz', 'value': [1, 2, 3]}, + ... {'op': 'remove', 'path': '/baz/1'}, + ... {'op': 'test', 'path': '/baz', 'value': [1, 3]}, + ... {'op': 'replace', 'path': '/baz/0', 'value': 42}, + ... {'op': 'remove', 'path': '/baz/1'}, + ... ]) + >>> doc = {} + >>> result = patch.apply(doc) + >>> expected = {'foo': 'bar', 'baz': [42]} + >>> result == expected + True + + JsonPatch object is iterable, so you can easily access each patch + statement in a loop: + + >>> lpatch = list(patch) + >>> expected = {'op': 'add', 'path': '/foo', 'value': 'bar'} + >>> lpatch[0] == expected + True + >>> lpatch == patch.patch + True + + Also JsonPatch could be converted directly to :class:`bool` if it contains + any operation statements: + + >>> bool(patch) + True + >>> bool(JsonPatch([])) + False + + This behavior is very handy with :func:`make_patch` to write more readable + code: + + >>> old = {'foo': 'bar', 'numbers': [1, 3, 4, 8]} + >>> new = {'baz': 'qux', 'numbers': [1, 4, 7]} + >>> patch = make_patch(old, new) + >>> if patch: + ... # document have changed, do something useful + ... patch.apply(old) #doctest: +ELLIPSIS + {...} + """ + def __init__(self, patch, pointer_cls=JsonPointer): + self.patch = patch + self.pointer_cls = pointer_cls + + # Verify that the structure of the patch document + # is correct by retrieving each patch element. + # Much of the validation is done in the initializer + # though some is delayed until the patch is applied. + for op in self.patch: + self._get_operation(op) + + def __str__(self): + """str(self) -> self.to_string()""" + return self.to_string() + + def __bool__(self): + return bool(self.patch) + + __nonzero__ = __bool__ + + 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 + + def __ne__(self, other): + return not(self == other) + + @classmethod + def from_string(cls, patch_str, loads=None, pointer_cls=JsonPointer): + """Creates JsonPatch instance from string source. + + :param patch_str: JSON patch as raw string. + :type patch_str: str + + :param loads: A function of one argument that loads a serialized + JSON string. + :type loads: function + + :param pointer_cls: JSON pointer class to use. + :type pointer_cls: Type[JsonPointer] + + :return: :class:`JsonPatch` instance. + """ + json_loader = loads or cls.json_loader + patch = json_loader(patch_str) + return cls(patch, pointer_cls=pointer_cls) + + @classmethod + def from_diff( + cls, src, dst, optimization=True, dumps=None, + pointer_cls=JsonPointer, + ): + """Creates JsonPatch instance based on comparison of two document + objects. Json patch would be created for `src` argument against `dst` + one. + + :param src: Data source document object. + :type src: dict + + :param dst: Data source document object. + :type dst: dict + + :param dumps: A function of one argument that produces a serialized + JSON string. + :type dumps: function + + :param pointer_cls: JSON pointer class to use. + :type pointer_cls: Type[JsonPointer] + + :return: :class:`JsonPatch` instance. + + >>> src = {'foo': 'bar', 'numbers': [1, 3, 4, 8]} + >>> dst = {'baz': 'qux', 'numbers': [1, 4, 7]} + >>> patch = JsonPatch.from_diff(src, dst) + >>> new = patch.apply(src) + >>> new == dst + True + """ + json_dumper = dumps or cls.json_dumper + builder = DiffBuilder(json_dumper, pointer_cls=pointer_cls) + builder._compare_values('', None, src, dst) + ops = list(builder.execute()) + return cls(ops, pointer_cls=pointer_cls) + + def to_string(self, dumps=None): + """Returns patch set as JSON string.""" + json_dumper = dumps or self.json_dumper + return json_dumper(self.patch) + + @property + def _ops(self): + return tuple(map(self._get_operation, self.patch)) + + def apply(self, obj, in_place=False): + """Applies the patch to a given object. + + :param obj: Document object. + :type obj: dict + + :param in_place: Tweaks the way how patch would be applied - directly to + specified `obj` or to its copy. + :type in_place: bool + + :return: Modified `obj`. + """ + + if not in_place: + obj = copy.deepcopy(obj) + + for operation in self._ops: + obj = operation.apply(obj) + + return obj + + def _get_operation(self, operation): + if 'op' not in operation: + raise InvalidJsonPatch("Operation does not contain 'op' member") + + op = operation['op'] + + if not isinstance(op, basestring): + raise InvalidJsonPatch("Operation must be a string") + + if op not in self.operations: + raise InvalidJsonPatch("Unknown operation {0!r}".format(op)) + + cls = self.operations[op] + return cls(operation, pointer_cls=self.pointer_cls) + + class DiffBuilder(object): def __init__(self, dumps=json.dumps, pointer_cls=JsonPointer): From 1268e09ffaead08f22184b63b3ad34bb41ab8bab Mon Sep 17 00:00:00 2001 From: Artyom Nikitin Date: Mon, 23 Nov 2020 23:53:48 +0300 Subject: [PATCH 24/47] test: custom operations --- tests.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests.py b/tests.py index b5b7b9a..0788d48 100755 --- a/tests.py +++ b/tests.py @@ -10,6 +10,11 @@ import jsonpatch import jsonpointer import sys +try: + from types import MappingProxyType +except ImportError: + # Python < 3.3 + MappingProxyType = dict class ApplyPatchTestCase(unittest.TestCase): @@ -938,6 +943,26 @@ def test_json_patch_with_prefix_pointer(self): self.assertEqual(res, {'foo': {'bar': {'baz': 'qux'}}}) +class CustomOperationTests(unittest.TestCase): + + def test_custom_operation(self): + + class IdentityOperation(jsonpatch.PatchOperation): + def apply(self, obj): + return obj + + class JsonPatch(jsonpatch.JsonPatch): + operations = MappingProxyType( + identity=IdentityOperation, + **jsonpatch.JsonPatch.operations + ) + + patch = JsonPatch([{'op': 'identity', 'path': '/'}]) + self.assertIn('identity', patch.operations) + res = patch.apply({}) + self.assertEqual(res, {}) + + if __name__ == '__main__': modules = ['jsonpatch'] @@ -956,6 +981,7 @@ def get_suite(): suite.addTest(unittest.makeSuite(JsonPatchCreationTest)) suite.addTest(unittest.makeSuite(UtilityMethodTests)) suite.addTest(unittest.makeSuite(CustomJsonPointerTests)) + suite.addTest(unittest.makeSuite(CustomOperationTests)) return suite From 9310d48af5bfcde50f9b05fdd43deeafec11c805 Mon Sep 17 00:00:00 2001 From: Artyom Nikitin Date: Tue, 24 Nov 2020 23:20:35 +0300 Subject: [PATCH 25/47] test: fix --- tests.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests.py b/tests.py index 0788d48..28fde9b 100755 --- a/tests.py +++ b/tests.py @@ -953,8 +953,10 @@ def apply(self, obj): class JsonPatch(jsonpatch.JsonPatch): operations = MappingProxyType( - identity=IdentityOperation, - **jsonpatch.JsonPatch.operations + dict( + identity=IdentityOperation, + **jsonpatch.JsonPatch.operations + ) ) patch = JsonPatch([{'op': 'identity', 'path': '/'}]) From a9a83b5aae65db3007fef8a4015f46e6e59d69c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Tue, 1 Dec 2020 20:54:26 +0100 Subject: [PATCH 26/47] Bump version to 1.28 --- jsonpatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonpatch.py b/jsonpatch.py index a01a177..84f6fb3 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -61,7 +61,7 @@ # Will be parsed by setup.py to determine package metadata __author__ = 'Stefan Kögl ' -__version__ = '1.24' +__version__ = '1.28' __website__ = 'https://github.com/stefankoegl/python-json-patch' __license__ = 'Modified BSD License' From 8d15ed5740027d5c0f295f82b347d963c77b8c5e Mon Sep 17 00:00:00 2001 From: Ryan Marvin Date: Mon, 1 Feb 2021 16:58:05 +0300 Subject: [PATCH 27/47] Fix make_patch --- jsonpatch.py | 9 ++++++--- tests.py | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/jsonpatch.py b/jsonpatch.py index 84f6fb3..14341d7 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -635,7 +635,7 @@ def from_diff( True """ json_dumper = dumps or cls.json_dumper - builder = DiffBuilder(json_dumper, pointer_cls=pointer_cls) + builder = DiffBuilder(src, dst, json_dumper, pointer_cls=pointer_cls) builder._compare_values('', None, src, dst) ops = list(builder.execute()) return cls(ops, pointer_cls=pointer_cls) @@ -688,12 +688,14 @@ def _get_operation(self, operation): class DiffBuilder(object): - def __init__(self, dumps=json.dumps, pointer_cls=JsonPointer): + def __init__(self, src_doc, dst_doc, dumps=json.dumps, pointer_cls=JsonPointer): self.dumps = dumps self.pointer_cls = pointer_cls self.index_storage = [{}, {}] self.index_storage2 = [[], []] self.__root = root = [] + self.src_doc = src_doc + self.dst_doc = dst_doc root[:] = [root, root, None] def store_index(self, value, index, st): @@ -800,7 +802,8 @@ def _item_removed(self, path, key, item): new_index = self.insert(new_op) if index is not None: op = index[2] - if type(op.key) == int: + added_item = op.pointer.to_last(self.dst_doc)[0] + if type(added_item) == list: for v in self.iter_from(index): op.key = v._on_undo_add(op.path, op.key) diff --git a/tests.py b/tests.py index 28fde9b..a56ffc0 100755 --- a/tests.py +++ b/tests.py @@ -490,6 +490,61 @@ def test_issue103(self): self.assertEqual(res, dst) self.assertIsInstance(res['A'], float) + def test_issue119(self): + """Make sure it avoids casting numeric str dict key to int""" + src = [ + {'foobar': {u'1': [u'lettuce', u'cabbage', u'bok choy', u'broccoli'], u'3': [u'ibex'], u'2': [u'apple'], u'5': [], u'4': [u'gerenuk', u'duiker'], u'10_1576156603109': [], u'6': [], u'8_1572034252560': [u'thompson', u'gravie', u'mango', u'coconut'], u'7_1572034204585': []}}, + {'foobar':{u'description': u'', u'title': u''}} + ] + dst = [ + {'foobar': {u'9': [u'almond'], u'10': u'yes', u'12': u'', u'16_1598876845275': [], u'7': [u'pecan']}}, + {'foobar': {u'1': [u'lettuce', u'cabbage', u'bok choy', u'broccoli'], u'3': [u'ibex'], u'2': [u'apple'], u'5': [], u'4': [u'gerenuk', u'duiker'], u'10_1576156603109': [], u'6': [], u'8_1572034252560': [u'thompson', u'gravie', u'mango', u'coconut'], u'7_1572034204585': []}}, + {'foobar': {u'description': u'', u'title': u''}} + ] + patch = jsonpatch.make_patch(src, dst) + res = jsonpatch.apply_patch(src, patch) + self.assertEqual(res, dst) + + def test_issue120(self): + """Make sure it avoids casting numeric str dict key to int""" + src = [{'foobar': {'821b7213_b9e6_2b73_2e9c_cf1526314553': ['Open Work'], + '6e3d1297_0c5a_88f9_576b_ad9216611c94': ['Many Things'], + '1987bcf0_dc97_59a1_4c62_ce33e51651c7': ['Product']}}, + {'foobar': {'2a7624e_0166_4d75_a92c_06b3f': []}}, + {'foobar': {'10': [], + '11': ['bee', + 'ant', + 'wasp'], + '13': ['phobos', + 'titan', + 'gaea'], + '14': [], + '15': 'run3', + '16': 'service', + '2': ['zero', 'enable']}}] + dst = [{'foobar': {'1': [], '2': []}}, + {'foobar': {'821b7213_b9e6_2b73_2e9c_cf1526314553': ['Open Work'], + '6e3d1297_0c5a_88f9_576b_ad9216611c94': ['Many Things'], + '1987bcf0_dc97_59a1_4c62_ce33e51651c7': ['Product']}}, + {'foobar': {'2a7624e_0166_4d75_a92c_06b3f': []}}, + {'foobar': {'b238d74d_dcf4_448c_9794_c13a2f7b3c0a': [], + 'dcb0387c2_f7ae_b8e5bab_a2b1_94deb7c': []}}, + {'foobar': {'10': [], + '11': ['bee', + 'ant', + 'fly'], + '13': ['titan', + 'phobos', + 'gaea'], + '14': [], + '15': 'run3', + '16': 'service', + '2': ['zero', 'enable']}} + ] + patch = jsonpatch.make_patch(src, dst) + res = jsonpatch.apply_patch(src, patch) + self.assertEqual(res, dst) + def test_custom_types_diff(self): old = {'value': decimal.Decimal('1.0')} new = {'value': decimal.Decimal('1.00')} From 78abec1651c4c3166d0eda4f9c0e43e00df57494 Mon Sep 17 00:00:00 2001 From: Ryan Marvin Date: Thu, 18 Feb 2021 18:25:25 +0300 Subject: [PATCH 28/47] Add comment --- jsonpatch.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/jsonpatch.py b/jsonpatch.py index 14341d7..7b895b7 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -802,6 +802,10 @@ def _item_removed(self, path, key, item): new_index = self.insert(new_op) if index is not None: op = index[2] + # We can't rely on the op.key property type since PatchOperation casts + # the .key property to int and this path wrongly ends up being taken + # for numeric string dict keys while the intention is to only handle lists. + # So we do an explicit check on the item affected by the op instead. added_item = op.pointer.to_last(self.dst_doc)[0] if type(added_item) == list: for v in self.iter_from(index): From f6b26b25805f1c01c3fae1495176cefac7d4a158 Mon Sep 17 00:00:00 2001 From: Ryan Marvin Date: Thu, 18 Feb 2021 18:27:21 +0300 Subject: [PATCH 29/47] Update comment --- jsonpatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonpatch.py b/jsonpatch.py index 7b895b7..b4ff24b 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -802,7 +802,7 @@ def _item_removed(self, path, key, item): new_index = self.insert(new_op) if index is not None: op = index[2] - # We can't rely on the op.key property type since PatchOperation casts + # We can't rely on the op.key type since PatchOperation casts # the .key property to int and this path wrongly ends up being taken # for numeric string dict keys while the intention is to only handle lists. # So we do an explicit check on the item affected by the op instead. From dbea3db33298da4ec41197b07612c42580e132e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Tue, 2 Mar 2021 21:25:16 +0100 Subject: [PATCH 30/47] Fix version number v1.30 --- jsonpatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonpatch.py b/jsonpatch.py index b4ff24b..429d40b 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -61,7 +61,7 @@ # Will be parsed by setup.py to determine package metadata __author__ = 'Stefan Kögl ' -__version__ = '1.28' +__version__ = '1.30' __website__ = 'https://github.com/stefankoegl/python-json-patch' __license__ = 'Modified BSD License' From 974d54f393de78ce21bee9897cee8f1ace5813ee Mon Sep 17 00:00:00 2001 From: Genzer Hawker Date: Wed, 3 Mar 2021 14:24:31 +0700 Subject: [PATCH 31/47] Add support for preserving Unicode characters in jsonpatch CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If the JSON content contains some Unicode characters, the jsonpatch final output will encode the Unicode character using ASCII (i.e `\u0394`). This behaviour comes from the module `json.dump()` governed by a flag `ensure_ascii`[1]. For example: ```json /* patch.json */ [{ "op": "add", "path": "/SomeUnicodeSamples", "value": "𝒞𝘋𝙴𝓕ĢȞỈ𝕵 đ áê 🤩 äÄöÖüÜß" }] ``` After applying the patch on an empty source file `{}`, this is the output: ```json {"SomeUnicodeSamples": "\ud835\udc9e\ud835\ude0b...\u00fc\u00dc\u00df"} ``` This commit adds a flag `-u|--preserve-unicode` in the jsonpatch CLI to configure the behaviour of `json.dump`'s `ensure_ascii` flag. Using the `--preserve-unicode` flag, the cli will print the Unicode characters as-is without any encoding. [1]: https://docs.python.org/3/library/json.html#basic-usage --- bin/jsonpatch | 7 ++++--- doc/commandline.rst | 10 ++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/bin/jsonpatch b/bin/jsonpatch index 3f01738..a7adf29 100755 --- a/bin/jsonpatch +++ b/bin/jsonpatch @@ -24,7 +24,8 @@ 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__) - +parser.add_argument('-u', '--preserve-unicode', action='store_true', + help='Output Unicode character as-is without using Code Point') def main(): try: @@ -72,8 +73,8 @@ def patch_files(): # By this point we have some sort of file object we can write the # modified JSON to. - - json.dump(result, fp, indent=args.indent) + + json.dump(result, fp, indent=args.indent, ensure_ascii=not(args.preserve_unicode)) fp.write('\n') if args.in_place: diff --git a/doc/commandline.rst b/doc/commandline.rst index 5644d08..5fb9a3c 100644 --- a/doc/commandline.rst +++ b/doc/commandline.rst @@ -74,10 +74,12 @@ The program ``jsonpatch`` is used to apply JSON patches on JSON files. :: PATCH Patch file optional arguments: - -h, --help show this help message and exit - --indent INDENT Indent output by n spaces - -v, --version show program's version number and exit - + -h, --help show this help message and exit + --indent INDENT Indent output by n spaces + -b, --backup Back up ORIGINAL if modifying in-place + -i, --in-place Modify ORIGINAL in-place instead of to stdout + -v, --version show program's version number and exit + -u, --preserve-unicode Output Unicode character as-is without using Code Point Example ^^^^^^^ From 7a6d76ada4b990b1951831a42a08924de5775c2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Thu, 4 Mar 2021 20:08:42 +0100 Subject: [PATCH 32/47] Remove failing pypy build --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 48f6882..865f8fa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,6 @@ python: - '3.9' - 3.10-dev - nightly -- pypy - pypy3 addons: apt: From d1cfec3187bc5e8b9e43127848107d7f4bf3dd80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Thu, 4 Mar 2021 20:13:29 +0100 Subject: [PATCH 33/47] Bump version to 1.31 --- jsonpatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonpatch.py b/jsonpatch.py index 429d40b..05d9a2b 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -61,7 +61,7 @@ # Will be parsed by setup.py to determine package metadata __author__ = 'Stefan Kögl ' -__version__ = '1.30' +__version__ = '1.31' __website__ = 'https://github.com/stefankoegl/python-json-patch' __license__ = 'Modified BSD License' From 5cdb066ab6bfd0f28e7bd78a61f13bf4ff90d00d Mon Sep 17 00:00:00 2001 From: Bock Date: Thu, 11 Mar 2021 17:15:15 -0700 Subject: [PATCH 34/47] closes #129 --- jsonpatch.py | 14 ++++++++------ tests.py | 9 +++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/jsonpatch.py b/jsonpatch.py index 429d40b..bd7701b 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -699,27 +699,29 @@ def __init__(self, src_doc, dst_doc, dumps=json.dumps, pointer_cls=JsonPointer): root[:] = [root, root, None] def store_index(self, value, index, st): + typed_key = (value, type(value)) try: storage = self.index_storage[st] - stored = storage.get(value) + stored = storage.get(typed_key) if stored is None: - storage[value] = [index] + storage[typed_key] = [index] else: - storage[value].append(index) + storage[typed_key].append(index) except TypeError: - self.index_storage2[st].append((value, index)) + self.index_storage2[st].append((typed_key, index)) def take_index(self, value, st): + typed_key = (value, type(value)) try: - stored = self.index_storage[st].get(value) + stored = self.index_storage[st].get(typed_key) if stored: return stored.pop() except TypeError: storage = self.index_storage2[st] for i in range(len(storage)-1, -1, -1): - if storage[i][0] == value: + if storage[i][0] == typed_key: return storage.pop(i)[1] def insert(self, op): diff --git a/tests.py b/tests.py index a56ffc0..8a638cf 100755 --- a/tests.py +++ b/tests.py @@ -481,6 +481,15 @@ def test_issue90(self): self.assertEqual(res, dst) self.assertIsInstance(res['A'], bool) + def test_issue129(self): + """In JSON 1 is different from True even though in python 1 == True Take Two""" + src = {'A': {'D': 1.0}, 'B': {'E': 'a'}} + dst = {'A': {'C': 'a'}, 'B': {'C': True}} + patch = jsonpatch.make_patch(src, dst) + res = jsonpatch.apply_patch(src, patch) + self.assertEqual(res, dst) + self.assertIsInstance(res['B']['C'], bool) + def test_issue103(self): """In JSON 1 is different from 1.0 even though in python 1 == 1.0""" src = {'A': 1} From 55d4816975350ea3f683814f4d025951ddfb1693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Sat, 13 Mar 2021 20:14:39 +0100 Subject: [PATCH 35/47] Bump version to 1.32 --- jsonpatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonpatch.py b/jsonpatch.py index 3ffe5fb..5213b32 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -61,7 +61,7 @@ # Will be parsed by setup.py to determine package metadata __author__ = 'Stefan Kögl ' -__version__ = '1.31' +__version__ = '1.32' __website__ = 'https://github.com/stefankoegl/python-json-patch' __license__ = 'Modified BSD License' From c9bfb91727690d6c7249b9250aba8942613f3f1c Mon Sep 17 00:00:00 2001 From: Julien Palard Date: Tue, 16 Mar 2021 15:33:47 +0100 Subject: [PATCH 36/47] FIX: TypeError when one forgot to put its operation in a list. --- jsonpatch.py | 15 ++++++++++++++- tests.py | 6 ++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/jsonpatch.py b/jsonpatch.py index 5213b32..1bced46 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -558,6 +558,19 @@ def __init__(self, patch, pointer_cls=JsonPointer): # Much of the validation is done in the initializer # though some is delayed until the patch is applied. for op in self.patch: + # We're only checking for basestring in the following check + # for two reasons: + # + # - It should come from JSON, which only allows strings as + # dictionary keys, so having a string here unambiguously means + # someone used: {"op": ..., ...} instead of [{"op": ..., ...}]. + # + # - There's no possible false positive: if someone give a sequence + # of mappings, this won't raise. + if isinstance(op, basestring): + raise InvalidJsonPatch("Document is expected to be sequence of " + "operations, got a sequence of strings.") + self._get_operation(op) def __str__(self): @@ -677,7 +690,7 @@ def _get_operation(self, operation): op = operation['op'] if not isinstance(op, basestring): - raise InvalidJsonPatch("Operation must be a string") + raise InvalidJsonPatch("Operation's op must be a string") if op not in self.operations: raise InvalidJsonPatch("Unknown operation {0!r}".format(op)) diff --git a/tests.py b/tests.py index 8a638cf..797c220 100755 --- a/tests.py +++ b/tests.py @@ -190,6 +190,12 @@ def test_test_not_existing(self): obj, [{'op': 'test', 'path': '/baz', 'value': 'bar'}]) + def test_forgetting_surrounding_list(self): + obj = {'bar': 'qux'} + self.assertRaises(jsonpatch.InvalidJsonPatch, + jsonpatch.apply_patch, + obj, {'op': 'test', 'path': '/bar'}) + def test_test_noval_existing(self): obj = {'bar': 'qux'} self.assertRaises(jsonpatch.InvalidJsonPatch, From db194f820dee88e1a66a811a7a8653cce6965bc3 Mon Sep 17 00:00:00 2001 From: Vu-Hoang Phan Date: Tue, 6 Apr 2021 13:33:18 +0200 Subject: [PATCH 37/47] fix invalid remove index --- jsonpatch.py | 10 ++++++++++ tests.py | 6 ++++++ 2 files changed, 16 insertions(+) diff --git a/jsonpatch.py b/jsonpatch.py index 1bced46..238a6c9 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -39,6 +39,12 @@ import functools import json import sys + +try: + from collections.abc import Mapping, Sequence +except ImportError: # Python 3 + from collections import Mapping, Sequence + try: from types import MappingProxyType except ImportError: @@ -234,6 +240,10 @@ class RemoveOperation(PatchOperation): def apply(self, obj): subobj, part = self.pointer.to_last(obj) + + if isinstance(subobj, Sequence) and not isinstance(part, int): + raise JsonPointerException("invalid array index '{0}'".format(part)) + try: del subobj[part] except (KeyError, IndexError) as ex: diff --git a/tests.py b/tests.py index 797c220..d9eea92 100755 --- a/tests.py +++ b/tests.py @@ -87,6 +87,12 @@ def test_remove_array_item(self): res = jsonpatch.apply_patch(obj, [{'op': 'remove', 'path': '/foo/1'}]) self.assertEqual(res['foo'], ['bar', 'baz']) + def test_remove_invalid_item(self): + obj = {'foo': ['bar', 'qux', 'baz']} + with self.assertRaises(jsonpointer.JsonPointerException): + jsonpatch.apply_patch(obj, [{'op': 'remove', 'path': '/foo/-'}]) + + def test_replace_object_key(self): obj = {'foo': 'bar', 'baz': 'qux'} res = jsonpatch.apply_patch(obj, [{'op': 'replace', 'path': '/baz', 'value': 'boo'}]) From 46eef55d5170c08dd9513c86703b365f3d51db3c Mon Sep 17 00:00:00 2001 From: Vu-Hoang Phan Date: Tue, 6 Apr 2021 13:54:00 +0200 Subject: [PATCH 38/47] remove unused import --- jsonpatch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jsonpatch.py b/jsonpatch.py index 238a6c9..a4bd519 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -41,9 +41,9 @@ import sys try: - from collections.abc import Mapping, Sequence + from collections.abc import Sequence except ImportError: # Python 3 - from collections import Mapping, Sequence + from collections import Sequence try: from types import MappingProxyType From 714df3c2102630a80691c4248b0b7babda5d128b Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Wed, 15 Sep 2021 04:10:27 +1000 Subject: [PATCH 39/47] docs: fix simple typo, raies -> raise (#135) --- ext_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext_tests.py b/ext_tests.py index 2770c8e..1fd8d8f 100755 --- a/ext_tests.py +++ b/ext_tests.py @@ -65,7 +65,7 @@ def _test(self, test): raise Exception(test.get('comment', '')) from jpe # if there is no 'expected' we only verify that applying the patch - # does not raies an exception + # does not raise an exception if 'expected' in test: self.assertEquals(res, test['expected'], test.get('comment', '')) From a76f742dcfb7a4b0fb0ab2bec4bb4e54a7ebb3ef Mon Sep 17 00:00:00 2001 From: Hiroshi Miura <6115787+hirmiura@users.noreply.github.com> Date: Sat, 17 Jun 2023 05:06:12 +0900 Subject: [PATCH 40/47] feat(jsondiff): Add support for preserving Unicode characters (#145) Mostly same as https://github.com/stefankoegl/python-json-patch/pull/127 --- bin/jsondiff | 4 +++- doc/commandline.rst | 9 +++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/bin/jsondiff b/bin/jsondiff index b79188b..5ac0090 100755 --- a/bin/jsondiff +++ b/bin/jsondiff @@ -14,6 +14,8 @@ parser.add_argument('FILE1', type=argparse.FileType('r')) parser.add_argument('FILE2', type=argparse.FileType('r')) parser.add_argument('--indent', type=int, default=None, help='Indent output by n spaces') +parser.add_argument('-u', '--preserve-unicode', action='store_true', + help='Output Unicode character as-is without using Code Point') parser.add_argument('-v', '--version', action='version', version='%(prog)s ' + jsonpatch.__version__) @@ -32,7 +34,7 @@ def diff_files(): doc2 = json.load(args.FILE2) patch = jsonpatch.make_patch(doc1, doc2) if patch.patch: - print(json.dumps(patch.patch, indent=args.indent)) + print(json.dumps(patch.patch, indent=args.indent, ensure_ascii=not(args.preserve_unicode))) sys.exit(1) if __name__ == "__main__": diff --git a/doc/commandline.rst b/doc/commandline.rst index 5fb9a3c..a7a78d8 100644 --- a/doc/commandline.rst +++ b/doc/commandline.rst @@ -10,7 +10,7 @@ The JSON patch package contains the commandline utilities ``jsondiff`` and The program ``jsondiff`` can be used to create a JSON patch by comparing two JSON files :: - usage: jsondiff [-h] [--indent INDENT] [-v] FILE1 FILE2 + usage: jsondiff [-h] [--indent INDENT] [-u] [-v] FILE1 FILE2 Diff two JSON files @@ -19,9 +19,10 @@ JSON files :: FILE2 optional arguments: - -h, --help show this help message and exit - --indent INDENT Indent output by n spaces - -v, --version show program's version number and exit + -h, --help show this help message and exit + --indent INDENT Indent output by n spaces + -u, --preserve-unicode Output Unicode character as-is without using Code Point + -v, --version show program's version number and exit Example ^^^^^^^ From 33562b0d685ced527ee635ac14bf01fbe3c94ad0 Mon Sep 17 00:00:00 2001 From: Tim Poulsen Date: Fri, 16 Jun 2023 16:07:28 -0400 Subject: [PATCH 41/47] Update license text to match official 3-clause-BSD (#142) --- COPYING | 26 -------------------------- LICENSE | 11 +++++++++++ 2 files changed, 11 insertions(+), 26 deletions(-) delete mode 100644 COPYING create mode 100644 LICENSE diff --git a/COPYING b/COPYING deleted file mode 100644 index 491196d..0000000 --- a/COPYING +++ /dev/null @@ -1,26 +0,0 @@ -Copyright (c) 2011 Stefan Kögl -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. -3. The name of the author may not be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR -IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c8fc60f --- /dev/null +++ b/LICENSE @@ -0,0 +1,11 @@ +Copyright (c) 2011 Stefan Kögl + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. From 45cfe90c84985ac50f6c34d6124294c2f0898379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Fri, 16 Jun 2023 22:41:43 +0200 Subject: [PATCH 42/47] Switch to GitHub actions (#144) * Switch to GitHub actions * add support for Python 3.11, remove 3.5, 3.6 --- .github/workflows/test.yaml | 34 ++++++++++++++++++++++++++++++++++ .travis.yml | 33 --------------------------------- setup.py | 4 +--- 3 files changed, 35 insertions(+), 36 deletions(-) create mode 100644 .github/workflows/test.yaml delete mode 100644 .travis.yml diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..639e18d --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,34 @@ +name: Python package + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["2.7", "3.7", "3.8", "3.9", "3.10", "3.11"] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install coveralls +# - name: Lint with flake8 +# run: | + # stop the build if there are Python syntax errors or undefined names + # flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test + run: | + coverage run --source=jsonpointer tests.py diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 865f8fa..0000000 --- a/.travis.yml +++ /dev/null @@ -1,33 +0,0 @@ -dist: xenial -language: python -python: -- '2.7' -- '3.5' -- '3.6' -- '3.7' -- '3.8' -- '3.9' -- 3.10-dev -- nightly -- pypy3 -addons: - apt: - packages: - - pandoc -install: -- travis_retry pip install -r requirements.txt -- travis_retry pip install coveralls -script: -- coverage run --source=jsonpointer tests.py -after_script: -- coveralls -before_deploy: -- pip install -r requirements-dev.txt -deploy: - provider: pypi - user: skoegl - password: - secure: ppMhKu82oIig1INyiNkt9veOd5FUUIKFUXj2TzxMSdzPtzAhQnScJMGPEtPfH8MwXng/CtJiDWS6zJzRFsW/3Ch+JHPkOtxOfkopBs1t1SpCyqNPSvf6Zxh83Dg6Bq6+8GyVW1RPuNIGflsvzY2C3z5i79FQXwZd8EQlg7Vu0Wo= - on: - tags: true - distributions: sdist bdist_wheel diff --git a/setup.py b/setup.py index ad43bd5..7753be1 100644 --- a/setup.py +++ b/setup.py @@ -56,8 +56,6 @@ 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', @@ -80,7 +78,7 @@ package_data={'': ['requirements.txt']}, scripts=['bin/jsondiff', 'bin/jsonpatch'], classifiers=CLASSIFIERS, - python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*', + python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*', project_urls={ 'Website': 'https://github.com/stefankoegl/python-json-patch', 'Repository': 'https://github.com/stefankoegl/python-json-patch.git', From 0b0520328504050ee09d835d4df294838e055c38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Fri, 16 Jun 2023 22:43:04 +0200 Subject: [PATCH 43/47] bump version to 1.33 --- jsonpatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonpatch.py b/jsonpatch.py index a4bd519..d3fc26d 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -67,7 +67,7 @@ # Will be parsed by setup.py to determine package metadata __author__ = 'Stefan Kögl ' -__version__ = '1.32' +__version__ = '1.33' __website__ = 'https://github.com/stefankoegl/python-json-patch' __license__ = 'Modified BSD License' From e5a007a76998b1a2309d8c6cfc474d7ff4a870de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20K=C3=B6gl?= Date: Fri, 16 Jun 2023 23:22:18 +0200 Subject: [PATCH 44/47] add .readthedocs.yaml https://blog.readthedocs.com/migrate-configuration-v2/ --- .readthedocs.yaml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..b8c2f2b --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,22 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: doc/conf.py + +# We recommend specifying your dependencies to enable reproducible builds: +# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +# python: +# install: +# - requirements: docs/requirements.txt From 73c36f2c4776c008cd4e750f5240e06dfdc918fc Mon Sep 17 00:00:00 2001 From: Andrew Garrett Date: Wed, 28 Jun 2023 14:01:39 +1000 Subject: [PATCH 45/47] Update documentation to include a link to the GitHub repo and installation instructions (#147) --- doc/index.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/doc/index.rst b/doc/index.rst index 2f46921..b97b82b 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -6,10 +6,15 @@ python-json-patch ================= -*python-json-patch* is a Python library for applying JSON patches (`RFC 6902 +`python-json-patch `_ +is a Python library for applying JSON patches (`RFC 6902 `_). Python 2.7 and 3.4+ are supported. Tests are run on both CPython and PyPy. +**Installation** +.. code-block:: bash + $ pip install jsonpatch +.. **Contents** From a22e05a16d746c772802b51c57bdae55b6564723 Mon Sep 17 00:00:00 2001 From: konstantin Date: Sat, 24 Feb 2024 17:04:16 +0100 Subject: [PATCH 46/47] chore: add Python 3.10-3.12 as supported versions (#156) --- setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.py b/setup.py index 7753be1..ab9f32a 100644 --- a/setup.py +++ b/setup.py @@ -59,6 +59,9 @@ 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: Libraries', From d8e1a6e244728c04229d601bc9a384d9b034c603 Mon Sep 17 00:00:00 2001 From: CyrilRoelandteNovance Date: Mon, 5 Aug 2024 22:10:52 +0200 Subject: [PATCH 47/47] Fix tests for Python 3.12 (#162) unittest.TestCase.assertEquals has been removed in Python 3.12; unittest.TestCase.assertEqual should be used instead[1]. [1] https://docs.python.org/3/whatsnew/3.12.html#id3 --- ext_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext_tests.py b/ext_tests.py index 1fd8d8f..59a36d2 100755 --- a/ext_tests.py +++ b/ext_tests.py @@ -67,7 +67,7 @@ def _test(self, test): # if there is no 'expected' we only verify that applying the patch # does not raise an exception if 'expected' in test: - self.assertEquals(res, test['expected'], test.get('comment', '')) + self.assertEqual(res, test['expected'], test.get('comment', '')) def make_test_case(tests):