From 499f5e49a09ead4ba9568e84e824a777c427e7fe Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Oct 2026 12:01:46 +0000 Subject: [PATCH 1/2] Remove dead code - PatchOperation.path, .key, get_part() and set_part(), and MoveOperation.from_path, .from_key, get_from_part() and set_from_part() kept the indices of the old DiffBuilder's operations up to date, and have had no callers since it was rewritten (#179) - _item_after() was only called with inserted=False, and never moved its location then either: make_patch compares the documents in order, so an earlier 'add' that a 'remove' turns into a move is in front of the removed item. For the same reason it is never inside it, so the check against moving a value into its own children is dropped from _item_removed() - _to_last() rejected pointers into strings for jsonpointer < 3.2, which is no longer supported; jsonpointer itself rejects them now make_patch gives the same patches as before, and applying patches gives the same results and exceptions. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015o6rtuujv61fERXZhQaL9i --- jsonpatch.py | 118 +++++++++------------------------------------------ 1 file changed, 19 insertions(+), 99 deletions(-) diff --git a/jsonpatch.py b/jsonpatch.py index 163f371..eaf4ca1 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -221,35 +221,12 @@ def __eq__(self, other): def __ne__(self, other): return not(self == other) - @property - def path(self): - return '/'.join(self.pointer.parts[:-1]) - - @property - def key(self): - return self.get_part(-1) - - @key.setter - def key(self, value): - self.set_part(-1, value) - - def get_part(self, index): - try: - return int(self.pointer.parts[index]) - except ValueError: - return self.pointer.parts[index] - - def set_part(self, index, value): - self.pointer.parts[index] = str(value) - self.location = self.pointer.path - self.operation['path'] = self.location - class RemoveOperation(PatchOperation): """Removes an object property or an array element.""" def apply(self, obj): - subobj, part = _to_last(self.pointer, obj) + subobj, part = self.pointer.to_last(obj) if part is None: raise JsonPatchConflict("can't remove the whole document") @@ -281,7 +258,7 @@ def apply(self, obj): return self._add(obj, copy.deepcopy(value)) def _add(self, obj, value): - subobj, part = _to_last(self.pointer, obj) + subobj, part = self.pointer.to_last(obj) if part is None: return value # we're replacing the root, whatever its type @@ -317,7 +294,7 @@ def apply(self, obj): # copied for the same reason as in AddOperation.apply value = copy.deepcopy(value) - subobj, part = _to_last(self.pointer, obj) + subobj, part = self.pointer.to_last(obj) if part is None: return value @@ -382,38 +359,13 @@ def apply(self, obj): return obj - @property - def from_path(self): - from_ptr = self.pointer_cls(self.operation['from']) - return '/'.join(from_ptr.parts[:-1]) - - @property - def from_key(self): - return self.get_from_part(-1) - - @from_key.setter - def from_key(self, value): - self.set_from_part(-1, value) - - def get_from_part(self, index): - from_ptr = self.pointer_cls(self.operation['from']) - try: - return int(from_ptr.parts[index]) - except ValueError: - return from_ptr.parts[index] - - def set_from_part(self, index, value): - from_ptr = self.pointer_cls(self.operation['from']) - from_ptr.parts[index] = str(value) - self.operation['from'] = from_ptr.path - class TestOperation(PatchOperation): """Test value by specified location.""" def apply(self, obj): try: - subobj, part = _to_last(self.pointer, obj) + subobj, part = self.pointer.to_last(obj) if part is None: val = subobj else: @@ -836,19 +788,18 @@ def _item_removed(self, path, key, item): if index is not None: added_location = index[2]['path'] added = self._parts(added_location) - moved_from = _without_item(source, added) - target = _item_after(added, source, False) - # RFC 6902 does not allow moving a value into its own children - if not _is_inside(target, moved_from): - self.remove(index) - if moved_from != target: - self._set_present(location, False) - self.insert({'op': 'move', 'from': location, - 'path': added_location}) - else: - # the removed item stays where the added one would be - self._set_present(added_location, False) - return + # The documents are compared in order, so the added item is in + # the part compared already: it is in front of the removed one, + # and neither inside it nor moved by removing it + self.remove(index) + if _without_item(source, added) != added: + self._set_present(location, False) + self.insert({'op': 'move', 'from': location, + 'path': added_location}) + else: + # the removed item stays where the added one would be + self._set_present(added_location, False) + return self._set_present(location, False) new_index = self.insert({'op': 'remove', 'path': location}) @@ -1186,44 +1137,13 @@ def _without_item(parts, location): return parts -def _item_after(location, parts, inserted): - """ Where the item at location is after inserting (or removing, if not - inserted) the value at parts """ - depth = len(parts) - 1 - if not isinstance(parts[-1], int) or not _is_inside(location, parts[:-1]): - return location - - if inserted and location[depth] >= parts[-1]: - return _shift(location, depth, 1) - - if not inserted and location[depth] > parts[-1]: - return _shift(location, depth, -1) - - return location - - -def _to_last(pointer, doc): - """Resolve pointer like JsonPointer.to_last, without indexing into strings. - - RFC 6901 only allows reference tokens to be applied to objects and arrays, - but older versions of jsonpointer treat strings as sequences. - """ - subobj, part = pointer.to_last(doc) - - if part is not None and isinstance(subobj, str): - raise JsonPointerException( - "Cannot apply token '{0}' to non-container type {1}".format( - part, type(subobj))) - - return subobj, part - - def _to_existing_last(pointer, doc): - """Resolve pointer like _to_last, for a location that has to exist. + """Resolve pointer like JsonPointer.to_last, for a location that has to + exist. The '-' of an array refers to the nonexistent element after its last one. """ - subobj, part = _to_last(pointer, doc) + subobj, part = pointer.to_last(doc) if part == '-' and isinstance(subobj, Sequence): raise JsonPointerException("invalid array index '{0}'".format(part)) From 8d28e99a5dd0dfcf87206dc9160b295aa0899927 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Oct 2026 14:46:24 +0000 Subject: [PATCH 2/2] Keep the PatchOperation and MoveOperation index helpers They are public, and may be used outside of jsonpatch. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015o6rtuujv61fERXZhQaL9i --- jsonpatch.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/jsonpatch.py b/jsonpatch.py index eaf4ca1..5cc742f 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -221,6 +221,29 @@ def __eq__(self, other): def __ne__(self, other): return not(self == other) + @property + def path(self): + return '/'.join(self.pointer.parts[:-1]) + + @property + def key(self): + return self.get_part(-1) + + @key.setter + def key(self, value): + self.set_part(-1, value) + + def get_part(self, index): + try: + return int(self.pointer.parts[index]) + except ValueError: + return self.pointer.parts[index] + + def set_part(self, index, value): + self.pointer.parts[index] = str(value) + self.location = self.pointer.path + self.operation['path'] = self.location + class RemoveOperation(PatchOperation): """Removes an object property or an array element.""" @@ -359,6 +382,31 @@ def apply(self, obj): return obj + @property + def from_path(self): + from_ptr = self.pointer_cls(self.operation['from']) + return '/'.join(from_ptr.parts[:-1]) + + @property + def from_key(self): + return self.get_from_part(-1) + + @from_key.setter + def from_key(self, value): + self.set_from_part(-1, value) + + def get_from_part(self, index): + from_ptr = self.pointer_cls(self.operation['from']) + try: + return int(from_ptr.parts[index]) + except ValueError: + return from_ptr.parts[index] + + def set_from_part(self, index, value): + from_ptr = self.pointer_cls(self.operation['from']) + from_ptr.parts[index] = str(value) + self.operation['from'] = from_ptr.path + class TestOperation(PatchOperation): """Test value by specified location."""