Skip to content
Merged
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
26 changes: 20 additions & 6 deletions patchdiff/pointer.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,27 @@ def evaluate(self, obj: Diffable) -> tuple[Diffable, Hashable, Any]:
parent = None
cursor = obj
if tokens := self.tokens:
# Walk to the parent strictly: any failure here is a path that
# doesn't exist in the target, and silently landing on a partial
# parent would let iapply write to the wrong place.
for key in tokens[:-1]:
parent = cursor
cursor = parent[key]
# The leaf may legitimately not exist (add ops on dicts, list
# "-" append) so we tolerate lookup failures there — but only
# when the parent is itself a container we can write into.
parent = cursor
key = tokens[-1]
try:
for key in tokens:
parent = cursor
cursor = parent[key]
except (KeyError, TypeError):
# KeyError for dicts, TypeError for sets and lists
pass
cursor = parent[key]
except (KeyError, IndexError, TypeError):
if not (
hasattr(parent, "keys")
or hasattr(parent, "append")
or hasattr(parent, "add")
):
raise
cursor = None
return parent, key, cursor

def append(self, token: Hashable) -> "Pointer":
Expand Down
21 changes: 21 additions & 0 deletions tests/test_apply.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import pytest

from patchdiff import apply, diff
from patchdiff.pointer import Pointer


def test_apply():
Expand Down Expand Up @@ -132,3 +135,21 @@ def test_add_remove_list_extended_inverse_leaving_end():

d = apply(b, rops)
assert a == d


def test_apply_raises_on_missing_dict_key():
ops = [{"op": "replace", "path": Pointer(["missing", "key"]), "value": 99}]
with pytest.raises(KeyError):
apply({"present": 1}, ops)


def test_apply_raises_on_out_of_range_list_index():
ops = [{"op": "replace", "path": Pointer([10, "x"]), "value": 99}]
with pytest.raises(IndexError):
apply([1, 2, 3], ops)


def test_apply_raises_when_traversing_into_primitive():
ops = [{"op": "replace", "path": Pointer(["a", "b"]), "value": 99}]
with pytest.raises(TypeError):
apply({"a": 5}, ops)
17 changes: 17 additions & 0 deletions tests/test_pointer.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import pytest

from patchdiff.pointer import Pointer


Expand Down Expand Up @@ -44,3 +46,18 @@ def test_pointer_eq():

def test_pointer_append():
assert Pointer([1]).append("foo") == Pointer([1, "foo"])


def test_pointer_evaluate_raises_on_missing_dict_key():
with pytest.raises(KeyError):
Pointer(["missing", "key"]).evaluate({"present": 1})


def test_pointer_evaluate_raises_on_out_of_range_list_index():
with pytest.raises(IndexError):
Pointer([10, "x"]).evaluate([1, 2, 3])


def test_pointer_evaluate_raises_when_traversing_into_primitive():
with pytest.raises(TypeError):
Pointer(["a", "b"]).evaluate({"a": 5})