jsonpatch.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931
  1. # -*- coding: utf-8 -*-
  2. #
  3. # python-json-patch - An implementation of the JSON Patch format
  4. # https://github.com/stefankoegl/python-json-patch
  5. #
  6. # Copyright (c) 2011 Stefan Kögl <stefan@skoegl.net>
  7. # All rights reserved.
  8. #
  9. # Redistribution and use in source and binary forms, with or without
  10. # modification, are permitted provided that the following conditions
  11. # are met:
  12. #
  13. # 1. Redistributions of source code must retain the above copyright
  14. # notice, this list of conditions and the following disclaimer.
  15. # 2. Redistributions in binary form must reproduce the above copyright
  16. # notice, this list of conditions and the following disclaimer in the
  17. # documentation and/or other materials provided with the distribution.
  18. # 3. The name of the author may not be used to endorse or promote products
  19. # derived from this software without specific prior written permission.
  20. #
  21. # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
  22. # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
  23. # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
  24. # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
  25. # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
  26. # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  27. # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  28. # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  29. # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
  30. # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  31. #
  32. """ Apply JSON-Patches (RFC 6902) """
  33. from __future__ import unicode_literals
  34. import collections
  35. import copy
  36. import functools
  37. import json
  38. import sys
  39. try:
  40. from collections.abc import Sequence
  41. except ImportError: # Python 3
  42. from collections import Sequence
  43. try:
  44. from types import MappingProxyType
  45. except ImportError:
  46. # Python < 3.3
  47. MappingProxyType = dict
  48. from jsonpointer import JsonPointer, JsonPointerException
  49. _ST_ADD = 0
  50. _ST_REMOVE = 1
  51. try:
  52. from collections.abc import MutableMapping, MutableSequence
  53. except ImportError:
  54. from collections import MutableMapping, MutableSequence
  55. str = unicode
  56. # Will be parsed by setup.py to determine package metadata
  57. __author__ = 'Stefan Kögl <stefan@skoegl.net>'
  58. __version__ = '1.33'
  59. __website__ = 'https://github.com/stefankoegl/python-json-patch'
  60. __license__ = 'Modified BSD License'
  61. # pylint: disable=E0611,W0404
  62. if sys.version_info >= (3, 0):
  63. basestring = (bytes, str) # pylint: disable=C0103,W0622
  64. class JsonPatchException(Exception):
  65. """Base Json Patch exception"""
  66. class InvalidJsonPatch(JsonPatchException):
  67. """ Raised if an invalid JSON Patch is created """
  68. class JsonPatchConflict(JsonPatchException):
  69. """Raised if patch could not be applied due to conflict situation such as:
  70. - attempt to add object key when it already exists;
  71. - attempt to operate with nonexistence object key;
  72. - attempt to insert value to array at position beyond its size;
  73. - etc.
  74. """
  75. class JsonPatchTestFailed(JsonPatchException, AssertionError):
  76. """ A Test operation failed """
  77. def multidict(ordered_pairs):
  78. """Convert duplicate keys values to lists."""
  79. # read all values into lists
  80. mdict = collections.defaultdict(list)
  81. for key, value in ordered_pairs:
  82. mdict[key].append(value)
  83. return dict(
  84. # unpack lists that have only 1 item
  85. (key, values[0] if len(values) == 1 else values)
  86. for key, values in mdict.items()
  87. )
  88. # The "object_pairs_hook" parameter is used to handle duplicate keys when
  89. # loading a JSON object.
  90. _jsonloads = functools.partial(json.loads, object_pairs_hook=multidict)
  91. def apply_patch(doc, patch, in_place=False, pointer_cls=JsonPointer):
  92. """Apply list of patches to specified json document.
  93. :param doc: Document object.
  94. :type doc: dict
  95. :param patch: JSON patch as list of dicts or raw JSON-encoded string.
  96. :type patch: list or str
  97. :param in_place: While :const:`True` patch will modify target document.
  98. By default patch will be applied to document copy.
  99. :type in_place: bool
  100. :param pointer_cls: JSON pointer class to use.
  101. :type pointer_cls: Type[JsonPointer]
  102. :return: Patched document object.
  103. :rtype: dict
  104. >>> doc = {'foo': 'bar'}
  105. >>> patch = [{'op': 'add', 'path': '/baz', 'value': 'qux'}]
  106. >>> other = apply_patch(doc, patch)
  107. >>> doc is not other
  108. True
  109. >>> other == {'foo': 'bar', 'baz': 'qux'}
  110. True
  111. >>> patch = [{'op': 'add', 'path': '/baz', 'value': 'qux'}]
  112. >>> apply_patch(doc, patch, in_place=True) == {'foo': 'bar', 'baz': 'qux'}
  113. True
  114. >>> doc == other
  115. True
  116. """
  117. if isinstance(patch, basestring):
  118. patch = JsonPatch.from_string(patch, pointer_cls=pointer_cls)
  119. else:
  120. patch = JsonPatch(patch, pointer_cls=pointer_cls)
  121. return patch.apply(doc, in_place)
  122. def make_patch(src, dst, pointer_cls=JsonPointer):
  123. """Generates patch by comparing two document objects. Actually is
  124. a proxy to :meth:`JsonPatch.from_diff` method.
  125. :param src: Data source document object.
  126. :type src: dict
  127. :param dst: Data source document object.
  128. :type dst: dict
  129. :param pointer_cls: JSON pointer class to use.
  130. :type pointer_cls: Type[JsonPointer]
  131. >>> src = {'foo': 'bar', 'numbers': [1, 3, 4, 8]}
  132. >>> dst = {'baz': 'qux', 'numbers': [1, 4, 7]}
  133. >>> patch = make_patch(src, dst)
  134. >>> new = patch.apply(src)
  135. >>> new == dst
  136. True
  137. """
  138. return JsonPatch.from_diff(src, dst, pointer_cls=pointer_cls)
  139. class PatchOperation(object):
  140. """A single operation inside a JSON Patch."""
  141. def __init__(self, operation, pointer_cls=JsonPointer):
  142. self.pointer_cls = pointer_cls
  143. if not operation.__contains__('path'):
  144. raise InvalidJsonPatch("Operation must have a 'path' member")
  145. if isinstance(operation['path'], self.pointer_cls):
  146. self.location = operation['path'].path
  147. self.pointer = operation['path']
  148. else:
  149. self.location = operation['path']
  150. try:
  151. self.pointer = self.pointer_cls(self.location)
  152. except TypeError as ex:
  153. raise InvalidJsonPatch("Invalid 'path'")
  154. self.operation = operation
  155. def apply(self, obj):
  156. """Abstract method that applies a patch operation to the specified object."""
  157. raise NotImplementedError('should implement the patch operation.')
  158. def __hash__(self):
  159. return hash(frozenset(self.operation.items()))
  160. def __eq__(self, other):
  161. if not isinstance(other, PatchOperation):
  162. return False
  163. return self.operation == other.operation
  164. def __ne__(self, other):
  165. return not(self == other)
  166. @property
  167. def path(self):
  168. return '/'.join(self.pointer.parts[:-1])
  169. @property
  170. def key(self):
  171. try:
  172. return int(self.pointer.parts[-1])
  173. except ValueError:
  174. return self.pointer.parts[-1]
  175. @key.setter
  176. def key(self, value):
  177. self.pointer.parts[-1] = str(value)
  178. self.location = self.pointer.path
  179. self.operation['path'] = self.location
  180. class RemoveOperation(PatchOperation):
  181. """Removes an object property or an array element."""
  182. def apply(self, obj):
  183. subobj, part = self.pointer.to_last(obj)
  184. if isinstance(subobj, Sequence) and not isinstance(part, int):
  185. raise JsonPointerException("invalid array index '{0}'".format(part))
  186. try:
  187. del subobj[part]
  188. except (KeyError, IndexError) as ex:
  189. msg = "can't remove a non-existent object '{0}'".format(part)
  190. raise JsonPatchConflict(msg)
  191. return obj
  192. def _on_undo_remove(self, path, key):
  193. if self.path == path:
  194. if self.key >= key:
  195. self.key += 1
  196. else:
  197. key -= 1
  198. return key
  199. def _on_undo_add(self, path, key):
  200. if self.path == path:
  201. if self.key > key:
  202. self.key -= 1
  203. else:
  204. key -= 1
  205. return key
  206. class AddOperation(PatchOperation):
  207. """Adds an object property or an array element."""
  208. def apply(self, obj):
  209. try:
  210. value = self.operation["value"]
  211. except KeyError as ex:
  212. raise InvalidJsonPatch(
  213. "The operation does not contain a 'value' member")
  214. subobj, part = self.pointer.to_last(obj)
  215. if isinstance(subobj, MutableSequence):
  216. if part == '-':
  217. subobj.append(value) # pylint: disable=E1103
  218. elif part > len(subobj) or part < 0:
  219. raise JsonPatchConflict("can't insert outside of list")
  220. else:
  221. subobj.insert(part, value) # pylint: disable=E1103
  222. elif isinstance(subobj, MutableMapping):
  223. if part is None:
  224. obj = value # we're replacing the root
  225. else:
  226. subobj[part] = value
  227. else:
  228. if part is None:
  229. raise TypeError("invalid document type {0}".format(type(subobj)))
  230. else:
  231. raise JsonPatchConflict("unable to fully resolve json pointer {0}, part {1}".format(self.location, part))
  232. return obj
  233. def _on_undo_remove(self, path, key):
  234. if self.path == path:
  235. if self.key > key:
  236. self.key += 1
  237. else:
  238. key += 1
  239. return key
  240. def _on_undo_add(self, path, key):
  241. if self.path == path:
  242. if self.key > key:
  243. self.key -= 1
  244. else:
  245. key += 1
  246. return key
  247. class ReplaceOperation(PatchOperation):
  248. """Replaces an object property or an array element by a new value."""
  249. def apply(self, obj):
  250. try:
  251. value = self.operation["value"]
  252. except KeyError as ex:
  253. raise InvalidJsonPatch(
  254. "The operation does not contain a 'value' member")
  255. subobj, part = self.pointer.to_last(obj)
  256. if part is None:
  257. return value
  258. if part == "-":
  259. raise InvalidJsonPatch("'path' with '-' can't be applied to 'replace' operation")
  260. if isinstance(subobj, MutableSequence):
  261. if part >= len(subobj) or part < 0:
  262. raise JsonPatchConflict("can't replace outside of list")
  263. elif isinstance(subobj, MutableMapping):
  264. if part not in subobj:
  265. msg = "can't replace a non-existent object '{0}'".format(part)
  266. raise JsonPatchConflict(msg)
  267. else:
  268. if part is None:
  269. raise TypeError("invalid document type {0}".format(type(subobj)))
  270. else:
  271. raise JsonPatchConflict("unable to fully resolve json pointer {0}, part {1}".format(self.location, part))
  272. subobj[part] = value
  273. return obj
  274. def _on_undo_remove(self, path, key):
  275. return key
  276. def _on_undo_add(self, path, key):
  277. return key
  278. class MoveOperation(PatchOperation):
  279. """Moves an object property or an array element to a new location."""
  280. def apply(self, obj):
  281. try:
  282. if isinstance(self.operation['from'], self.pointer_cls):
  283. from_ptr = self.operation['from']
  284. else:
  285. from_ptr = self.pointer_cls(self.operation['from'])
  286. except KeyError as ex:
  287. raise InvalidJsonPatch(
  288. "The operation does not contain a 'from' member")
  289. subobj, part = from_ptr.to_last(obj)
  290. try:
  291. value = subobj[part]
  292. except (KeyError, IndexError) as ex:
  293. raise JsonPatchConflict(str(ex))
  294. # If source and target are equal, this is a no-op
  295. if self.pointer == from_ptr:
  296. return obj
  297. if isinstance(subobj, MutableMapping) and \
  298. self.pointer.contains(from_ptr):
  299. raise JsonPatchConflict('Cannot move values into their own children')
  300. obj = RemoveOperation({
  301. 'op': 'remove',
  302. 'path': self.operation['from']
  303. }, pointer_cls=self.pointer_cls).apply(obj)
  304. obj = AddOperation({
  305. 'op': 'add',
  306. 'path': self.location,
  307. 'value': value
  308. }, pointer_cls=self.pointer_cls).apply(obj)
  309. return obj
  310. @property
  311. def from_path(self):
  312. from_ptr = self.pointer_cls(self.operation['from'])
  313. return '/'.join(from_ptr.parts[:-1])
  314. @property
  315. def from_key(self):
  316. from_ptr = self.pointer_cls(self.operation['from'])
  317. try:
  318. return int(from_ptr.parts[-1])
  319. except TypeError:
  320. return from_ptr.parts[-1]
  321. @from_key.setter
  322. def from_key(self, value):
  323. from_ptr = self.pointer_cls(self.operation['from'])
  324. from_ptr.parts[-1] = str(value)
  325. self.operation['from'] = from_ptr.path
  326. def _on_undo_remove(self, path, key):
  327. if self.from_path == path:
  328. if self.from_key >= key:
  329. self.from_key += 1
  330. else:
  331. key -= 1
  332. if self.path == path:
  333. if self.key > key:
  334. self.key += 1
  335. else:
  336. key += 1
  337. return key
  338. def _on_undo_add(self, path, key):
  339. if self.from_path == path:
  340. if self.from_key > key:
  341. self.from_key -= 1
  342. else:
  343. key -= 1
  344. if self.path == path:
  345. if self.key > key:
  346. self.key -= 1
  347. else:
  348. key += 1
  349. return key
  350. class TestOperation(PatchOperation):
  351. """Test value by specified location."""
  352. def apply(self, obj):
  353. try:
  354. subobj, part = self.pointer.to_last(obj)
  355. if part is None:
  356. val = subobj
  357. else:
  358. val = self.pointer.walk(subobj, part)
  359. except JsonPointerException as ex:
  360. raise JsonPatchTestFailed(str(ex))
  361. try:
  362. value = self.operation['value']
  363. except KeyError as ex:
  364. raise InvalidJsonPatch(
  365. "The operation does not contain a 'value' member")
  366. if val != value:
  367. msg = '{0} ({1}) is not equal to tested value {2} ({3})'
  368. raise JsonPatchTestFailed(msg.format(val, type(val),
  369. value, type(value)))
  370. return obj
  371. class CopyOperation(PatchOperation):
  372. """ Copies an object property or an array element to a new location """
  373. def apply(self, obj):
  374. try:
  375. from_ptr = self.pointer_cls(self.operation['from'])
  376. except KeyError as ex:
  377. raise InvalidJsonPatch(
  378. "The operation does not contain a 'from' member")
  379. subobj, part = from_ptr.to_last(obj)
  380. try:
  381. value = copy.deepcopy(subobj[part])
  382. except (KeyError, IndexError) as ex:
  383. raise JsonPatchConflict(str(ex))
  384. obj = AddOperation({
  385. 'op': 'add',
  386. 'path': self.location,
  387. 'value': value
  388. }, pointer_cls=self.pointer_cls).apply(obj)
  389. return obj
  390. class JsonPatch(object):
  391. json_dumper = staticmethod(json.dumps)
  392. json_loader = staticmethod(_jsonloads)
  393. operations = MappingProxyType({
  394. 'remove': RemoveOperation,
  395. 'add': AddOperation,
  396. 'replace': ReplaceOperation,
  397. 'move': MoveOperation,
  398. 'test': TestOperation,
  399. 'copy': CopyOperation,
  400. })
  401. """A JSON Patch is a list of Patch Operations.
  402. >>> patch = JsonPatch([
  403. ... {'op': 'add', 'path': '/foo', 'value': 'bar'},
  404. ... {'op': 'add', 'path': '/baz', 'value': [1, 2, 3]},
  405. ... {'op': 'remove', 'path': '/baz/1'},
  406. ... {'op': 'test', 'path': '/baz', 'value': [1, 3]},
  407. ... {'op': 'replace', 'path': '/baz/0', 'value': 42},
  408. ... {'op': 'remove', 'path': '/baz/1'},
  409. ... ])
  410. >>> doc = {}
  411. >>> result = patch.apply(doc)
  412. >>> expected = {'foo': 'bar', 'baz': [42]}
  413. >>> result == expected
  414. True
  415. JsonPatch object is iterable, so you can easily access each patch
  416. statement in a loop:
  417. >>> lpatch = list(patch)
  418. >>> expected = {'op': 'add', 'path': '/foo', 'value': 'bar'}
  419. >>> lpatch[0] == expected
  420. True
  421. >>> lpatch == patch.patch
  422. True
  423. Also JsonPatch could be converted directly to :class:`bool` if it contains
  424. any operation statements:
  425. >>> bool(patch)
  426. True
  427. >>> bool(JsonPatch([]))
  428. False
  429. This behavior is very handy with :func:`make_patch` to write more readable
  430. code:
  431. >>> old = {'foo': 'bar', 'numbers': [1, 3, 4, 8]}
  432. >>> new = {'baz': 'qux', 'numbers': [1, 4, 7]}
  433. >>> patch = make_patch(old, new)
  434. >>> if patch:
  435. ... # document have changed, do something useful
  436. ... patch.apply(old) #doctest: +ELLIPSIS
  437. {...}
  438. """
  439. def __init__(self, patch, pointer_cls=JsonPointer):
  440. self.patch = patch
  441. self.pointer_cls = pointer_cls
  442. # Verify that the structure of the patch document
  443. # is correct by retrieving each patch element.
  444. # Much of the validation is done in the initializer
  445. # though some is delayed until the patch is applied.
  446. for op in self.patch:
  447. # We're only checking for basestring in the following check
  448. # for two reasons:
  449. #
  450. # - It should come from JSON, which only allows strings as
  451. # dictionary keys, so having a string here unambiguously means
  452. # someone used: {"op": ..., ...} instead of [{"op": ..., ...}].
  453. #
  454. # - There's no possible false positive: if someone give a sequence
  455. # of mappings, this won't raise.
  456. if isinstance(op, basestring):
  457. raise InvalidJsonPatch("Document is expected to be sequence of "
  458. "operations, got a sequence of strings.")
  459. self._get_operation(op)
  460. def __str__(self):
  461. """str(self) -> self.to_string()"""
  462. return self.to_string()
  463. def __bool__(self):
  464. return bool(self.patch)
  465. __nonzero__ = __bool__
  466. def __iter__(self):
  467. return iter(self.patch)
  468. def __hash__(self):
  469. return hash(tuple(self._ops))
  470. def __eq__(self, other):
  471. if not isinstance(other, JsonPatch):
  472. return False
  473. return self._ops == other._ops
  474. def __ne__(self, other):
  475. return not(self == other)
  476. @classmethod
  477. def from_string(cls, patch_str, loads=None, pointer_cls=JsonPointer):
  478. """Creates JsonPatch instance from string source.
  479. :param patch_str: JSON patch as raw string.
  480. :type patch_str: str
  481. :param loads: A function of one argument that loads a serialized
  482. JSON string.
  483. :type loads: function
  484. :param pointer_cls: JSON pointer class to use.
  485. :type pointer_cls: Type[JsonPointer]
  486. :return: :class:`JsonPatch` instance.
  487. """
  488. json_loader = loads or cls.json_loader
  489. patch = json_loader(patch_str)
  490. return cls(patch, pointer_cls=pointer_cls)
  491. @classmethod
  492. def from_diff(
  493. cls, src, dst, optimization=True, dumps=None,
  494. pointer_cls=JsonPointer,
  495. ):
  496. """Creates JsonPatch instance based on comparison of two document
  497. objects. Json patch would be created for `src` argument against `dst`
  498. one.
  499. :param src: Data source document object.
  500. :type src: dict
  501. :param dst: Data source document object.
  502. :type dst: dict
  503. :param dumps: A function of one argument that produces a serialized
  504. JSON string.
  505. :type dumps: function
  506. :param pointer_cls: JSON pointer class to use.
  507. :type pointer_cls: Type[JsonPointer]
  508. :return: :class:`JsonPatch` instance.
  509. >>> src = {'foo': 'bar', 'numbers': [1, 3, 4, 8]}
  510. >>> dst = {'baz': 'qux', 'numbers': [1, 4, 7]}
  511. >>> patch = JsonPatch.from_diff(src, dst)
  512. >>> new = patch.apply(src)
  513. >>> new == dst
  514. True
  515. """
  516. json_dumper = dumps or cls.json_dumper
  517. builder = DiffBuilder(src, dst, json_dumper, pointer_cls=pointer_cls)
  518. builder._compare_values('', None, src, dst)
  519. ops = list(builder.execute())
  520. return cls(ops, pointer_cls=pointer_cls)
  521. def to_string(self, dumps=None):
  522. """Returns patch set as JSON string."""
  523. json_dumper = dumps or self.json_dumper
  524. return json_dumper(self.patch)
  525. @property
  526. def _ops(self):
  527. return tuple(map(self._get_operation, self.patch))
  528. def apply(self, obj, in_place=False):
  529. """Applies the patch to a given object.
  530. :param obj: Document object.
  531. :type obj: dict
  532. :param in_place: Tweaks the way how patch would be applied - directly to
  533. specified `obj` or to its copy.
  534. :type in_place: bool
  535. :return: Modified `obj`.
  536. """
  537. if not in_place:
  538. obj = copy.deepcopy(obj)
  539. for operation in self._ops:
  540. obj = operation.apply(obj)
  541. return obj
  542. def _get_operation(self, operation):
  543. if 'op' not in operation:
  544. raise InvalidJsonPatch("Operation does not contain 'op' member")
  545. op = operation['op']
  546. if not isinstance(op, basestring):
  547. raise InvalidJsonPatch("Operation's op must be a string")
  548. if op not in self.operations:
  549. raise InvalidJsonPatch("Unknown operation {0!r}".format(op))
  550. cls = self.operations[op]
  551. return cls(operation, pointer_cls=self.pointer_cls)
  552. class DiffBuilder(object):
  553. def __init__(self, src_doc, dst_doc, dumps=json.dumps, pointer_cls=JsonPointer):
  554. self.dumps = dumps
  555. self.pointer_cls = pointer_cls
  556. self.index_storage = [{}, {}]
  557. self.index_storage2 = [[], []]
  558. self.__root = root = []
  559. self.src_doc = src_doc
  560. self.dst_doc = dst_doc
  561. root[:] = [root, root, None]
  562. def store_index(self, value, index, st):
  563. typed_key = (value, type(value))
  564. try:
  565. storage = self.index_storage[st]
  566. stored = storage.get(typed_key)
  567. if stored is None:
  568. storage[typed_key] = [index]
  569. else:
  570. storage[typed_key].append(index)
  571. except TypeError:
  572. self.index_storage2[st].append((typed_key, index))
  573. def take_index(self, value, st):
  574. typed_key = (value, type(value))
  575. try:
  576. stored = self.index_storage[st].get(typed_key)
  577. if stored:
  578. return stored.pop()
  579. except TypeError:
  580. storage = self.index_storage2[st]
  581. for i in range(len(storage)-1, -1, -1):
  582. if storage[i][0] == typed_key:
  583. return storage.pop(i)[1]
  584. def insert(self, op):
  585. root = self.__root
  586. last = root[0]
  587. last[1] = root[0] = [last, root, op]
  588. return root[0]
  589. def remove(self, index):
  590. link_prev, link_next, _ = index
  591. link_prev[1] = link_next
  592. link_next[0] = link_prev
  593. index[:] = []
  594. def iter_from(self, start):
  595. root = self.__root
  596. curr = start[1]
  597. while curr is not root:
  598. yield curr[2]
  599. curr = curr[1]
  600. def __iter__(self):
  601. root = self.__root
  602. curr = root[1]
  603. while curr is not root:
  604. yield curr[2]
  605. curr = curr[1]
  606. def execute(self):
  607. root = self.__root
  608. curr = root[1]
  609. while curr is not root:
  610. if curr[1] is not root:
  611. op_first, op_second = curr[2], curr[1][2]
  612. if op_first.location == op_second.location and \
  613. type(op_first) == RemoveOperation and \
  614. type(op_second) == AddOperation:
  615. yield ReplaceOperation({
  616. 'op': 'replace',
  617. 'path': op_second.location,
  618. 'value': op_second.operation['value'],
  619. }, pointer_cls=self.pointer_cls).operation
  620. curr = curr[1][1]
  621. continue
  622. yield curr[2].operation
  623. curr = curr[1]
  624. def _item_added(self, path, key, item):
  625. index = self.take_index(item, _ST_REMOVE)
  626. if index is not None:
  627. op = index[2]
  628. if type(op.key) == int and type(key) == int:
  629. for v in self.iter_from(index):
  630. op.key = v._on_undo_remove(op.path, op.key)
  631. self.remove(index)
  632. if op.location != _path_join(path, key):
  633. new_op = MoveOperation({
  634. 'op': 'move',
  635. 'from': op.location,
  636. 'path': _path_join(path, key),
  637. }, pointer_cls=self.pointer_cls)
  638. self.insert(new_op)
  639. else:
  640. new_op = AddOperation({
  641. 'op': 'add',
  642. 'path': _path_join(path, key),
  643. 'value': item,
  644. }, pointer_cls=self.pointer_cls)
  645. new_index = self.insert(new_op)
  646. self.store_index(item, new_index, _ST_ADD)
  647. def _item_removed(self, path, key, item):
  648. new_op = RemoveOperation({
  649. 'op': 'remove',
  650. 'path': _path_join(path, key),
  651. }, pointer_cls=self.pointer_cls)
  652. index = self.take_index(item, _ST_ADD)
  653. new_index = self.insert(new_op)
  654. if index is not None:
  655. op = index[2]
  656. # We can't rely on the op.key type since PatchOperation casts
  657. # the .key property to int and this path wrongly ends up being taken
  658. # for numeric string dict keys while the intention is to only handle lists.
  659. # So we do an explicit check on the item affected by the op instead.
  660. added_item = op.pointer.to_last(self.dst_doc)[0]
  661. if type(added_item) == list:
  662. for v in self.iter_from(index):
  663. op.key = v._on_undo_add(op.path, op.key)
  664. self.remove(index)
  665. if new_op.location != op.location:
  666. new_op = MoveOperation({
  667. 'op': 'move',
  668. 'from': new_op.location,
  669. 'path': op.location,
  670. }, pointer_cls=self.pointer_cls)
  671. new_index[2] = new_op
  672. else:
  673. self.remove(new_index)
  674. else:
  675. self.store_index(item, new_index, _ST_REMOVE)
  676. def _item_replaced(self, path, key, item):
  677. self.insert(ReplaceOperation({
  678. 'op': 'replace',
  679. 'path': _path_join(path, key),
  680. 'value': item,
  681. }, pointer_cls=self.pointer_cls))
  682. def _compare_dicts(self, path, src, dst):
  683. src_keys = set(src.keys())
  684. dst_keys = set(dst.keys())
  685. added_keys = dst_keys - src_keys
  686. removed_keys = src_keys - dst_keys
  687. for key in removed_keys:
  688. self._item_removed(path, str(key), src[key])
  689. for key in added_keys:
  690. self._item_added(path, str(key), dst[key])
  691. for key in src_keys & dst_keys:
  692. self._compare_values(path, key, src[key], dst[key])
  693. def _compare_lists(self, path, src, dst):
  694. len_src, len_dst = len(src), len(dst)
  695. max_len = max(len_src, len_dst)
  696. min_len = min(len_src, len_dst)
  697. for key in range(max_len):
  698. if key < min_len:
  699. old, new = src[key], dst[key]
  700. if old == new:
  701. continue
  702. elif isinstance(old, MutableMapping) and \
  703. isinstance(new, MutableMapping):
  704. self._compare_dicts(_path_join(path, key), old, new)
  705. elif isinstance(old, MutableSequence) and \
  706. isinstance(new, MutableSequence):
  707. self._compare_lists(_path_join(path, key), old, new)
  708. else:
  709. self._item_removed(path, key, old)
  710. self._item_added(path, key, new)
  711. elif len_src > len_dst:
  712. self._item_removed(path, len_dst, src[key])
  713. else:
  714. self._item_added(path, key, dst[key])
  715. def _compare_values(self, path, key, src, dst):
  716. if isinstance(src, MutableMapping) and \
  717. isinstance(dst, MutableMapping):
  718. self._compare_dicts(_path_join(path, key), src, dst)
  719. elif isinstance(src, MutableSequence) and \
  720. isinstance(dst, MutableSequence):
  721. self._compare_lists(_path_join(path, key), src, dst)
  722. # To ensure we catch changes to JSON, we can't rely on a simple
  723. # src == dst, because it would not recognize the difference between
  724. # 1 and True, among other things. Using json.dumps is the most
  725. # fool-proof way to ensure we catch type changes that matter to JSON
  726. # and ignore those that don't. The performance of this could be
  727. # improved by doing more direct type checks, but we'd need to be
  728. # careful to accept type changes that don't matter when JSONified.
  729. elif self.dumps(src) == self.dumps(dst):
  730. return
  731. else:
  732. self._item_replaced(path, key, dst)
  733. def _path_join(path, key):
  734. if key is None:
  735. return path
  736. return path + '/' + str(key).replace('~', '~0').replace('/', '~1')