Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
jsonpatch.py932 linesDownload Raw Back to site-packages
1# -*- coding: utf-8 -*-2#3# python-json-patch - An implementation of the JSON Patch format4# https://github.com/stefankoegl/python-json-patch5#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 without10# modification, are permitted provided that the following conditions11# are met:12#13# 1. Redistributions of source code must retain the above copyright14#    notice, this list of conditions and the following disclaimer.15# 2. Redistributions in binary form must reproduce the above copyright16#    notice, this list of conditions and the following disclaimer in the17#    documentation and/or other materials provided with the distribution.18# 3. The name of the author may not be used to endorse or promote products19#    derived from this software without specific prior written permission.20#21# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR22# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES23# 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, BUT26# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,27# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY28# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT29# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF30# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.31#32 33""" Apply JSON-Patches (RFC 6902) """34 35from __future__ import unicode_literals36 37import collections38import copy39import functools40import json41import sys42 43try:44    from collections.abc import Sequence45except ImportError:  # Python 346    from collections import Sequence47 48try:49    from types import MappingProxyType50except ImportError:51    # Python < 3.352    MappingProxyType = dict53 54from jsonpointer import JsonPointer, JsonPointerException55 56 57_ST_ADD = 058_ST_REMOVE = 159 60 61try:62    from collections.abc import MutableMapping, MutableSequence63 64except ImportError:65    from collections import MutableMapping, MutableSequence66    str = unicode67 68# Will be parsed by setup.py to determine package metadata69__author__ = 'Stefan Kögl <stefan@skoegl.net>'70__version__ = '1.33'71__website__ = 'https://github.com/stefankoegl/python-json-patch'72__license__ = 'Modified BSD License'73 74 75# pylint: disable=E0611,W040476if sys.version_info >= (3, 0):77    basestring = (bytes, str)  # pylint: disable=C0103,W062278 79 80class JsonPatchException(Exception):81    """Base Json Patch exception"""82 83 84class InvalidJsonPatch(JsonPatchException):85    """ Raised if an invalid JSON Patch is created """86 87 88class JsonPatchConflict(JsonPatchException):89    """Raised if patch could not be applied due to conflict situation such as:90    - attempt to add object key when it already exists;91    - attempt to operate with nonexistence object key;92    - attempt to insert value to array at position beyond its size;93    - etc.94    """95 96 97class JsonPatchTestFailed(JsonPatchException, AssertionError):98    """ A Test operation failed """99 100 101def multidict(ordered_pairs):102    """Convert duplicate keys values to lists."""103    # read all values into lists104    mdict = collections.defaultdict(list)105    for key, value in ordered_pairs:106        mdict[key].append(value)107 108    return dict(109        # unpack lists that have only 1 item110        (key, values[0] if len(values) == 1 else values)111        for key, values in mdict.items()112    )113 114 115# The "object_pairs_hook" parameter is used to handle duplicate keys when116# loading a JSON object.117_jsonloads = functools.partial(json.loads, object_pairs_hook=multidict)118 119 120def apply_patch(doc, patch, in_place=False, pointer_cls=JsonPointer):121    """Apply list of patches to specified json document.122 123    :param doc: Document object.124    :type doc: dict125 126    :param patch: JSON patch as list of dicts or raw JSON-encoded string.127    :type patch: list or str128 129    :param in_place: While :const:`True` patch will modify target document.130                     By default patch will be applied to document copy.131    :type in_place: bool132 133    :param pointer_cls: JSON pointer class to use.134    :type pointer_cls: Type[JsonPointer]135 136    :return: Patched document object.137    :rtype: dict138 139    >>> doc = {'foo': 'bar'}140    >>> patch = [{'op': 'add', 'path': '/baz', 'value': 'qux'}]141    >>> other = apply_patch(doc, patch)142    >>> doc is not other143    True144    >>> other == {'foo': 'bar', 'baz': 'qux'}145    True146    >>> patch = [{'op': 'add', 'path': '/baz', 'value': 'qux'}]147    >>> apply_patch(doc, patch, in_place=True) == {'foo': 'bar', 'baz': 'qux'}148    True149    >>> doc == other150    True151    """152 153    if isinstance(patch, basestring):154        patch = JsonPatch.from_string(patch, pointer_cls=pointer_cls)155    else:156        patch = JsonPatch(patch, pointer_cls=pointer_cls)157    return patch.apply(doc, in_place)158 159 160def make_patch(src, dst, pointer_cls=JsonPointer):161    """Generates patch by comparing two document objects. Actually is162    a proxy to :meth:`JsonPatch.from_diff` method.163 164    :param src: Data source document object.165    :type src: dict166 167    :param dst: Data source document object.168    :type dst: dict169 170    :param pointer_cls: JSON pointer class to use.171    :type pointer_cls: Type[JsonPointer]172 173    >>> src = {'foo': 'bar', 'numbers': [1, 3, 4, 8]}174    >>> dst = {'baz': 'qux', 'numbers': [1, 4, 7]}175    >>> patch = make_patch(src, dst)176    >>> new = patch.apply(src)177    >>> new == dst178    True179    """180 181    return JsonPatch.from_diff(src, dst, pointer_cls=pointer_cls)182 183 184class PatchOperation(object):185    """A single operation inside a JSON Patch."""186 187    def __init__(self, operation, pointer_cls=JsonPointer):188        self.pointer_cls = pointer_cls189 190        if not operation.__contains__('path'):191            raise InvalidJsonPatch("Operation must have a 'path' member")192 193        if isinstance(operation['path'], self.pointer_cls):194            self.location = operation['path'].path195            self.pointer = operation['path']196        else:197            self.location = operation['path']198            try:199                self.pointer = self.pointer_cls(self.location)200            except TypeError as ex:201                raise InvalidJsonPatch("Invalid 'path'")202 203        self.operation = operation204 205    def apply(self, obj):206        """Abstract method that applies a patch operation to the specified object."""207        raise NotImplementedError('should implement the patch operation.')208 209    def __hash__(self):210        return hash(frozenset(self.operation.items()))211 212    def __eq__(self, other):213        if not isinstance(other, PatchOperation):214            return False215        return self.operation == other.operation216 217    def __ne__(self, other):218        return not(self == other)219 220    @property221    def path(self):222        return '/'.join(self.pointer.parts[:-1])223 224    @property225    def key(self):226        try:227            return int(self.pointer.parts[-1])228        except ValueError:229            return self.pointer.parts[-1]230 231    @key.setter232    def key(self, value):233        self.pointer.parts[-1] = str(value)234        self.location = self.pointer.path235        self.operation['path'] = self.location236 237 238class RemoveOperation(PatchOperation):239    """Removes an object property or an array element."""240 241    def apply(self, obj):242        subobj, part = self.pointer.to_last(obj)243 244        if isinstance(subobj, Sequence) and not isinstance(part, int):245            raise JsonPointerException("invalid array index '{0}'".format(part))246 247        try:248            del subobj[part]249        except (KeyError, IndexError) as ex:250            msg = "can't remove a non-existent object '{0}'".format(part)251            raise JsonPatchConflict(msg)252 253        return obj254 255    def _on_undo_remove(self, path, key):256        if self.path == path:257            if self.key >= key:258                self.key += 1259            else:260                key -= 1261        return key262 263    def _on_undo_add(self, path, key):264        if self.path == path:265            if self.key > key:266                self.key -= 1267            else:268                key -= 1269        return key270 271 272class AddOperation(PatchOperation):273    """Adds an object property or an array element."""274 275    def apply(self, obj):276        try:277            value = self.operation["value"]278        except KeyError as ex:279            raise InvalidJsonPatch(280                "The operation does not contain a 'value' member")281 282        subobj, part = self.pointer.to_last(obj)283 284        if isinstance(subobj, MutableSequence):285            if part == '-':286                subobj.append(value)  # pylint: disable=E1103287 288            elif part > len(subobj) or part < 0:289                raise JsonPatchConflict("can't insert outside of list")290 291            else:292                subobj.insert(part, value)  # pylint: disable=E1103293 294        elif isinstance(subobj, MutableMapping):295            if part is None:296                obj = value  # we're replacing the root297            else:298                subobj[part] = value299 300        else:301            if part is None:302                raise TypeError("invalid document type {0}".format(type(subobj)))303            else:304                raise JsonPatchConflict("unable to fully resolve json pointer {0}, part {1}".format(self.location, part))305        return obj306 307    def _on_undo_remove(self, path, key):308        if self.path == path:309            if self.key > key:310                self.key += 1311            else:312                key += 1313        return key314 315    def _on_undo_add(self, path, key):316        if self.path == path:317            if self.key > key:318                self.key -= 1319            else:320                key += 1321        return key322 323 324class ReplaceOperation(PatchOperation):325    """Replaces an object property or an array element by a new value."""326 327    def apply(self, obj):328        try:329            value = self.operation["value"]330        except KeyError as ex:331            raise InvalidJsonPatch(332                "The operation does not contain a 'value' member")333 334        subobj, part = self.pointer.to_last(obj)335 336        if part is None:337            return value338 339        if part == "-":340            raise InvalidJsonPatch("'path' with '-' can't be applied to 'replace' operation")341 342        if isinstance(subobj, MutableSequence):343            if part >= len(subobj) or part < 0:344                raise JsonPatchConflict("can't replace outside of list")345 346        elif isinstance(subobj, MutableMapping):347            if part not in subobj:348                msg = "can't replace a non-existent object '{0}'".format(part)349                raise JsonPatchConflict(msg)350        else:351            if part is None:352                raise TypeError("invalid document type {0}".format(type(subobj)))353            else:354                raise JsonPatchConflict("unable to fully resolve json pointer {0}, part {1}".format(self.location, part))355 356        subobj[part] = value357        return obj358 359    def _on_undo_remove(self, path, key):360        return key361 362    def _on_undo_add(self, path, key):363        return key364 365 366class MoveOperation(PatchOperation):367    """Moves an object property or an array element to a new location."""368 369    def apply(self, obj):370        try:371            if isinstance(self.operation['from'], self.pointer_cls):372                from_ptr = self.operation['from']373            else:374                from_ptr = self.pointer_cls(self.operation['from'])375        except KeyError as ex:376            raise InvalidJsonPatch(377                "The operation does not contain a 'from' member")378 379        subobj, part = from_ptr.to_last(obj)380        try:381            value = subobj[part]382        except (KeyError, IndexError) as ex:383            raise JsonPatchConflict(str(ex))384 385        # If source and target are equal, this is a no-op386        if self.pointer == from_ptr:387            return obj388 389        if isinstance(subobj, MutableMapping) and \390                self.pointer.contains(from_ptr):391            raise JsonPatchConflict('Cannot move values into their own children')392 393        obj = RemoveOperation({394            'op': 'remove',395            'path': self.operation['from']396        }, pointer_cls=self.pointer_cls).apply(obj)397 398        obj = AddOperation({399            'op': 'add',400            'path': self.location,401            'value': value402        }, pointer_cls=self.pointer_cls).apply(obj)403 404        return obj405 406    @property407    def from_path(self):408        from_ptr = self.pointer_cls(self.operation['from'])409        return '/'.join(from_ptr.parts[:-1])410 411    @property412    def from_key(self):413        from_ptr = self.pointer_cls(self.operation['from'])414        try:415            return int(from_ptr.parts[-1])416        except TypeError:417            return from_ptr.parts[-1]418 419    @from_key.setter420    def from_key(self, value):421        from_ptr = self.pointer_cls(self.operation['from'])422        from_ptr.parts[-1] = str(value)423        self.operation['from'] = from_ptr.path424 425    def _on_undo_remove(self, path, key):426        if self.from_path == path:427            if self.from_key >= key:428                self.from_key += 1429            else:430                key -= 1431        if self.path == path:432            if self.key > key:433                self.key += 1434            else:435                key += 1436        return key437 438    def _on_undo_add(self, path, key):439        if self.from_path == path:440            if self.from_key > key:441                self.from_key -= 1442            else:443                key -= 1444        if self.path == path:445            if self.key > key:446                self.key -= 1447            else:448                key += 1449        return key450 451 452class TestOperation(PatchOperation):453    """Test value by specified location."""454 455    def apply(self, obj):456        try:457            subobj, part = self.pointer.to_last(obj)458            if part is None:459                val = subobj460            else:461                val = self.pointer.walk(subobj, part)462        except JsonPointerException as ex:463            raise JsonPatchTestFailed(str(ex))464 465        try:466            value = self.operation['value']467        except KeyError as ex:468            raise InvalidJsonPatch(469                "The operation does not contain a 'value' member")470 471        if val != value:472            msg = '{0} ({1}) is not equal to tested value {2} ({3})'473            raise JsonPatchTestFailed(msg.format(val, type(val),474                                                 value, type(value)))475 476        return obj477 478 479class CopyOperation(PatchOperation):480    """ Copies an object property or an array element to a new location """481 482    def apply(self, obj):483        try:484            from_ptr = self.pointer_cls(self.operation['from'])485        except KeyError as ex:486            raise InvalidJsonPatch(487                "The operation does not contain a 'from' member")488 489        subobj, part = from_ptr.to_last(obj)490        try:491            value = copy.deepcopy(subobj[part])492        except (KeyError, IndexError) as ex:493            raise JsonPatchConflict(str(ex))494 495        obj = AddOperation({496            'op': 'add',497            'path': self.location,498            'value': value499        }, pointer_cls=self.pointer_cls).apply(obj)500 501        return obj502 503 504class JsonPatch(object):505    json_dumper = staticmethod(json.dumps)506    json_loader = staticmethod(_jsonloads)507 508    operations = MappingProxyType({509        'remove': RemoveOperation,510        'add': AddOperation,511        'replace': ReplaceOperation,512        'move': MoveOperation,513        'test': TestOperation,514        'copy': CopyOperation,515    })516 517    """A JSON Patch is a list of Patch Operations.518 519    >>> patch = JsonPatch([520    ...     {'op': 'add', 'path': '/foo', 'value': 'bar'},521    ...     {'op': 'add', 'path': '/baz', 'value': [1, 2, 3]},522    ...     {'op': 'remove', 'path': '/baz/1'},523    ...     {'op': 'test', 'path': '/baz', 'value': [1, 3]},524    ...     {'op': 'replace', 'path': '/baz/0', 'value': 42},525    ...     {'op': 'remove', 'path': '/baz/1'},526    ... ])527    >>> doc = {}528    >>> result = patch.apply(doc)529    >>> expected = {'foo': 'bar', 'baz': [42]}530    >>> result == expected531    True532 533    JsonPatch object is iterable, so you can easily access each patch534    statement in a loop:535 536    >>> lpatch = list(patch)537    >>> expected = {'op': 'add', 'path': '/foo', 'value': 'bar'}538    >>> lpatch[0] == expected539    True540    >>> lpatch == patch.patch541    True542 543    Also JsonPatch could be converted directly to :class:`bool` if it contains544    any operation statements:545 546    >>> bool(patch)547    True548    >>> bool(JsonPatch([]))549    False550 551    This behavior is very handy with :func:`make_patch` to write more readable552    code:553 554    >>> old = {'foo': 'bar', 'numbers': [1, 3, 4, 8]}555    >>> new = {'baz': 'qux', 'numbers': [1, 4, 7]}556    >>> patch = make_patch(old, new)557    >>> if patch:558    ...     # document have changed, do something useful559    ...     patch.apply(old)    #doctest: +ELLIPSIS560    {...}561    """562    def __init__(self, patch, pointer_cls=JsonPointer):563        self.patch = patch564        self.pointer_cls = pointer_cls565 566        # Verify that the structure of the patch document567        # is correct by retrieving each patch element.568        # Much of the validation is done in the initializer569        # though some is delayed until the patch is applied.570        for op in self.patch:571            # We're only checking for basestring in the following check572            # for two reasons:573            #574            # - It should come from JSON, which only allows strings as575            #   dictionary keys, so having a string here unambiguously means576            #   someone used: {"op": ..., ...} instead of [{"op": ..., ...}].577            #578            # - There's no possible false positive: if someone give a sequence579            #   of mappings, this won't raise.580            if isinstance(op, basestring):581                raise InvalidJsonPatch("Document is expected to be sequence of "582                                       "operations, got a sequence of strings.")583 584            self._get_operation(op)585 586    def __str__(self):587        """str(self) -> self.to_string()"""588        return self.to_string()589 590    def __bool__(self):591        return bool(self.patch)592 593    __nonzero__ = __bool__594 595    def __iter__(self):596        return iter(self.patch)597 598    def __hash__(self):599        return hash(tuple(self._ops))600 601    def __eq__(self, other):602        if not isinstance(other, JsonPatch):603            return False604        return self._ops == other._ops605 606    def __ne__(self, other):607        return not(self == other)608 609    @classmethod610    def from_string(cls, patch_str, loads=None, pointer_cls=JsonPointer):611        """Creates JsonPatch instance from string source.612 613        :param patch_str: JSON patch as raw string.614        :type patch_str: str615 616        :param loads: A function of one argument that loads a serialized617                      JSON string.618        :type loads: function619 620        :param pointer_cls: JSON pointer class to use.621        :type pointer_cls: Type[JsonPointer]622 623        :return: :class:`JsonPatch` instance.624        """625        json_loader = loads or cls.json_loader626        patch = json_loader(patch_str)627        return cls(patch, pointer_cls=pointer_cls)628 629    @classmethod630    def from_diff(631            cls, src, dst, optimization=True, dumps=None,632            pointer_cls=JsonPointer,633    ):634        """Creates JsonPatch instance based on comparison of two document635        objects. Json patch would be created for `src` argument against `dst`636        one.637 638        :param src: Data source document object.639        :type src: dict640 641        :param dst: Data source document object.642        :type dst: dict643 644        :param dumps: A function of one argument that produces a serialized645                      JSON string.646        :type dumps: function647 648        :param pointer_cls: JSON pointer class to use.649        :type pointer_cls: Type[JsonPointer]650 651        :return: :class:`JsonPatch` instance.652 653        >>> src = {'foo': 'bar', 'numbers': [1, 3, 4, 8]}654        >>> dst = {'baz': 'qux', 'numbers': [1, 4, 7]}655        >>> patch = JsonPatch.from_diff(src, dst)656        >>> new = patch.apply(src)657        >>> new == dst658        True659        """660        json_dumper = dumps or cls.json_dumper661        builder = DiffBuilder(src, dst, json_dumper, pointer_cls=pointer_cls)662        builder._compare_values('', None, src, dst)663        ops = list(builder.execute())664        return cls(ops, pointer_cls=pointer_cls)665 666    def to_string(self, dumps=None):667        """Returns patch set as JSON string."""668        json_dumper = dumps or self.json_dumper669        return json_dumper(self.patch)670 671    @property672    def _ops(self):673        return tuple(map(self._get_operation, self.patch))674 675    def apply(self, obj, in_place=False):676        """Applies the patch to a given object.677 678        :param obj: Document object.679        :type obj: dict680 681        :param in_place: Tweaks the way how patch would be applied - directly to682                         specified `obj` or to its copy.683        :type in_place: bool684 685        :return: Modified `obj`.686        """687 688        if not in_place:689            obj = copy.deepcopy(obj)690 691        for operation in self._ops:692            obj = operation.apply(obj)693 694        return obj695 696    def _get_operation(self, operation):697        if 'op' not in operation:698            raise InvalidJsonPatch("Operation does not contain 'op' member")699 700        op = operation['op']701 702        if not isinstance(op, basestring):703            raise InvalidJsonPatch("Operation's op must be a string")704 705        if op not in self.operations:706            raise InvalidJsonPatch("Unknown operation {0!r}".format(op))707 708        cls = self.operations[op]709        return cls(operation, pointer_cls=self.pointer_cls)710 711 712class DiffBuilder(object):713 714    def __init__(self, src_doc, dst_doc, dumps=json.dumps, pointer_cls=JsonPointer):715        self.dumps = dumps716        self.pointer_cls = pointer_cls717        self.index_storage = [{}, {}]718        self.index_storage2 = [[], []]719        self.__root = root = []720        self.src_doc = src_doc721        self.dst_doc = dst_doc722        root[:] = [root, root, None]723 724    def store_index(self, value, index, st):725        typed_key = (value, type(value))726        try:727            storage = self.index_storage[st]728            stored = storage.get(typed_key)729            if stored is None:730                storage[typed_key] = [index]731            else:732                storage[typed_key].append(index)733 734        except TypeError:735            self.index_storage2[st].append((typed_key, index))736 737    def take_index(self, value, st):738        typed_key = (value, type(value))739        try:740            stored = self.index_storage[st].get(typed_key)741            if stored:742                return stored.pop()743 744        except TypeError:745            storage = self.index_storage2[st]746            for i in range(len(storage)-1, -1, -1):747                if storage[i][0] == typed_key:748                    return storage.pop(i)[1]749 750    def insert(self, op):751        root = self.__root752        last = root[0]753        last[1] = root[0] = [last, root, op]754        return root[0]755 756    def remove(self, index):757        link_prev, link_next, _ = index758        link_prev[1] = link_next759        link_next[0] = link_prev760        index[:] = []761 762    def iter_from(self, start):763        root = self.__root764        curr = start[1]765        while curr is not root:766            yield curr[2]767            curr = curr[1]768 769    def __iter__(self):770        root = self.__root771        curr = root[1]772        while curr is not root:773            yield curr[2]774            curr = curr[1]775 776    def execute(self):777        root = self.__root778        curr = root[1]779        while curr is not root:780            if curr[1] is not root:781                op_first, op_second = curr[2], curr[1][2]782                if op_first.location == op_second.location and \783                        type(op_first) == RemoveOperation and \784                        type(op_second) == AddOperation:785                    yield ReplaceOperation({786                        'op': 'replace',787                        'path': op_second.location,788                        'value': op_second.operation['value'],789                    }, pointer_cls=self.pointer_cls).operation790                    curr = curr[1][1]791                    continue792 793            yield curr[2].operation794            curr = curr[1]795 796    def _item_added(self, path, key, item):797        index = self.take_index(item, _ST_REMOVE)798        if index is not None:799            op = index[2]800            if type(op.key) == int and type(key) == int:801                for v in self.iter_from(index):802                    op.key = v._on_undo_remove(op.path, op.key)803 804            self.remove(index)805            if op.location != _path_join(path, key):806                new_op = MoveOperation({807                    'op': 'move',808                    'from': op.location,809                    'path': _path_join(path, key),810                }, pointer_cls=self.pointer_cls)811                self.insert(new_op)812        else:813            new_op = AddOperation({814                'op': 'add',815                'path': _path_join(path, key),816                'value': item,817            }, pointer_cls=self.pointer_cls)818            new_index = self.insert(new_op)819            self.store_index(item, new_index, _ST_ADD)820 821    def _item_removed(self, path, key, item):822        new_op = RemoveOperation({823            'op': 'remove',824            'path': _path_join(path, key),825        }, pointer_cls=self.pointer_cls)826        index = self.take_index(item, _ST_ADD)827        new_index = self.insert(new_op)828        if index is not None:829            op = index[2]830            # We can't rely on the op.key type since PatchOperation casts831            # the .key property to int and this path wrongly ends up being taken832            # for numeric string dict keys while the intention is to only handle lists.833            # So we do an explicit check on the item affected by the op instead.834            added_item = op.pointer.to_last(self.dst_doc)[0]835            if type(added_item) == list:836                for v in self.iter_from(index):837                    op.key = v._on_undo_add(op.path, op.key)838 839            self.remove(index)840            if new_op.location != op.location:841                new_op = MoveOperation({842                    'op': 'move',843                    'from': new_op.location,844                    'path': op.location,845                }, pointer_cls=self.pointer_cls)846                new_index[2] = new_op847 848            else:849                self.remove(new_index)850 851        else:852            self.store_index(item, new_index, _ST_REMOVE)853 854    def _item_replaced(self, path, key, item):855        self.insert(ReplaceOperation({856            'op': 'replace',857            'path': _path_join(path, key),858            'value': item,859        }, pointer_cls=self.pointer_cls))860 861    def _compare_dicts(self, path, src, dst):862        src_keys = set(src.keys())863        dst_keys = set(dst.keys())864        added_keys = dst_keys - src_keys865        removed_keys = src_keys - dst_keys866 867        for key in removed_keys:868            self._item_removed(path, str(key), src[key])869 870        for key in added_keys:871            self._item_added(path, str(key), dst[key])872 873        for key in src_keys & dst_keys:874            self._compare_values(path, key, src[key], dst[key])875 876    def _compare_lists(self, path, src, dst):877        len_src, len_dst = len(src), len(dst)878        max_len = max(len_src, len_dst)879        min_len = min(len_src, len_dst)880        for key in range(max_len):881            if key < min_len:882                old, new = src[key], dst[key]883                if old == new:884                    continue885 886                elif isinstance(old, MutableMapping) and \887                    isinstance(new, MutableMapping):888                    self._compare_dicts(_path_join(path, key), old, new)889 890                elif isinstance(old, MutableSequence) and \891                        isinstance(new, MutableSequence):892                    self._compare_lists(_path_join(path, key), old, new)893 894                else:895                    self._item_removed(path, key, old)896                    self._item_added(path, key, new)897 898            elif len_src > len_dst:899                self._item_removed(path, len_dst, src[key])900 901            else:902                self._item_added(path, key, dst[key])903 904    def _compare_values(self, path, key, src, dst):905        if isinstance(src, MutableMapping) and \906                isinstance(dst, MutableMapping):907            self._compare_dicts(_path_join(path, key), src, dst)908 909        elif isinstance(src, MutableSequence) and \910                isinstance(dst, MutableSequence):911            self._compare_lists(_path_join(path, key), src, dst)912 913        # To ensure we catch changes to JSON, we can't rely on a simple914        # src == dst, because it would not recognize the difference between915        # 1 and True, among other things. Using json.dumps is the most916        # fool-proof way to ensure we catch type changes that matter to JSON917        # and ignore those that don't. The performance of this could be918        # improved by doing more direct type checks, but we'd need to be919        # careful to accept type changes that don't matter when JSONified.920        elif self.dumps(src) == self.dumps(dst):921            return922 923        else:924            self._item_replaced(path, key, dst)925 926 927def _path_join(path, key):928    if key is None:929        return path930 931    return path + '/' + str(key).replace('~', '~0').replace('/', '~1')932 
codekingpro/portable-devtools · Team Ai