Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion doc/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ Creating a Patch
----------------

Patches can be created in two ways. One way is to explicitly create a
``JsonPatch`` object from a list of operations. For convenience, the method
``JsonPatch`` object from an iterable of operation dictionaries, such as a
list, tuple, iterator, or generator. Non-sequence iterables are materialized
before validation so that the patch can be applied repeatedly.
For convenience, the method
``JsonPatch.from_string()`` accepts a string, parses it and constructs the
patch object from it.

Expand Down
20 changes: 12 additions & 8 deletions jsonpatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
import copy
import functools
import json
from collections.abc import Mapping, MutableMapping, MutableSequence, Sequence
from collections.abc import (Mapping, MutableMapping, MutableSequence,
Sequence)
from difflib import SequenceMatcher
from types import MappingProxyType

Expand Down Expand Up @@ -100,13 +101,13 @@ def multidict(ordered_pairs):


def apply_patch(doc, patch, in_place=False, pointer_cls=JsonPointer):
"""Apply list of patches to specified json document.
"""Apply patch operations to the specified JSON document.

:param doc: Document object.
:type doc: dict

:param patch: JSON patch as list of dicts or raw JSON-encoded string.
:type patch: list or str
:param patch: Iterable of operation dicts or a raw JSON-encoded string.
:type patch: iterable or str

:param in_place: While ``True`` patch will modify target document.
By default patch will be applied to document copy.
Expand Down Expand Up @@ -457,7 +458,11 @@ def apply(self, obj):


class JsonPatch(object):
"""A JSON Patch is a list of Patch Operations.
"""A JSON Patch is an iterable of Patch Operations.

Accepts sequences, iterators, generators, and other iterables of operation
dictionaries. Non-sequence inputs are materialized before validation so
the resulting patch can be applied repeatedly.

>>> patch = JsonPatch([
... {'op': 'add', 'path': '/foo', 'value': 'bar'},
Expand Down Expand Up @@ -515,21 +520,20 @@ class JsonPatch(object):
})

def __init__(self, patch, pointer_cls=JsonPointer):
self.patch = patch
self.pointer_cls = pointer_cls

if isinstance(patch, (str, bytes, Mapping)):
raise InvalidJsonPatch("Document is expected to be sequence of operations")
try:
patch_iter = iter(patch)
self.patch = patch if isinstance(patch, Sequence) else list(patch)
except TypeError:
raise InvalidJsonPatch("Document is expected to be sequence of operations")

# 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 patch_iter:
for op in self.patch:
# We're only checking for strings in the following check
# for two reasons:
#
Expand Down
45 changes: 45 additions & 0 deletions tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,51 @@

class ApplyPatchTestCase(unittest.TestCase):

def test_operation_iterators_remain_reusable_after_validation(self):
operations = [
{'op': 'add', 'path': '/a', 'value': 1},
{'op': 'replace', 'path': '/a', 'value': 2},
]
class OneShotIterable:
def __init__(self, ops):
self.iterator = iter(ops)

def __iter__(self):
return self.iterator

for name, factory in (
('iterator', iter),
('generator', lambda ops: (op for op in ops)),
('one-shot iterable', OneShotIterable),
):
with self.subTest(name=name):
patch = jsonpatch.JsonPatch(factory(operations))
self.assertEqual(list(patch), operations)
self.assertEqual(json.loads(patch.to_string()), operations)
self.assertEqual(patch.apply({}), {'a': 2})
self.assertEqual(patch.apply({}), {'a': 2})

def test_empty_operation_iterator_is_false(self):
patch = jsonpatch.JsonPatch(iter([]))
self.assertFalse(patch)
self.assertEqual(patch.to_string(), '[]')

def test_invalid_operation_iterator_is_rejected(self):
operations = iter([
{'op': 'add', 'path': '/a', 'value': 1},
{'op': 'not-an-operation', 'path': '/a'},
])
with self.assertRaises(jsonpatch.InvalidJsonPatch):
jsonpatch.JsonPatch(operations)

def test_sequence_operations_preserve_contents(self):
operations = [{'op': 'add', 'path': '/a', 'value': 1}]
for sequence in (operations, tuple(operations)):
with self.subTest(sequence=type(sequence).__name__):
patch = jsonpatch.JsonPatch(sequence)
self.assertEqual(list(patch), operations)
self.assertEqual(patch.apply({}), {'a': 1})

def test_diff_without_optimization_uses_basic_operations(self):
documents = [
({'old': {'value': 1}}, {'new': {'value': 1}}),
Expand Down
Loading