diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a37a1f..37fc14f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,17 @@ **Fixes** -- Fixed JSON pointers using negative indices. The JSON Pointer specification (RFC 6901) does not allow negative array indexes. We now raise a `JSONPointerIndexError` if a JSON Pointer attempts to resolve an array item with a negative index. See [#115](https://github.com/jg-rp/python-jsonpath/issues/115). For anyone needing JSON Pointers that support negative indexes, set `JSONPointer.min_int_index` to a suitably negative integer, like `JSONPointer.min_int_index = -(2**53) + 1`. +- Fixed JSON pointers with negative indices. + + Previously, negative indices were resolved against array-like values, but the JSON Pointer specification (RFC 6901) does not permit negative array indexes. We now raise a `JSONPointerIndexError` when a JSON Pointer attempts to resolve an array element using a negative index. + + For users who require negative indices in JSON Pointers, you can set `JSONPointer.min_int_index` to a suitably negative integer, like `JSONPointer.min_int_index = -(2**53) + 1`. + + See [#116](https://github.com/jg-rp/python-jsonpath/pull/116). + +- Fixed the JSON Patch `add` operation. + + Previously, a `JSONPatchError` was raised when pointing to an array index equal to the array's length. Now we append to arrays in such cases. See [#117](https://github.com/jg-rp/python-jsonpath/issues/117). ## Version 2.0.0 diff --git a/jsonpath/patch.py b/jsonpath/patch.py index 736b55f..54480e7 100644 --- a/jsonpath/patch.py +++ b/jsonpath/patch.py @@ -7,6 +7,7 @@ from abc import ABC from abc import abstractmethod from io import IOBase +from typing import Any from typing import Dict from typing import Iterable from typing import List @@ -70,7 +71,11 @@ def apply( if target == "-": parent.append(self.value) else: - raise JSONPatchError("index out of range") + index = self.path._index(target) # noqa: SLF001 + if index == len(parent): + parent.append(self.value) + else: + raise JSONPatchError("index out of range") else: parent.insert(int(target), self.value) elif isinstance(parent, MutableMapping): @@ -628,7 +633,7 @@ def test(self: Self, path: Union[str, JSONPointer], value: object) -> Self: def apply( self, - data: Union[str, IOBase, MutableSequence[object], MutableMapping[str, object]], + data: Union[str, IOBase, MutableSequence[Any], MutableMapping[str, Any]], ) -> object: """Apply all operations from this patch to _data_. @@ -676,7 +681,7 @@ def asdicts(self) -> List[Dict[str, object]]: def apply( patch: Union[str, IOBase, Iterable[Mapping[str, object]], None], - data: Union[str, IOBase, MutableSequence[object], MutableMapping[str, object]], + data: Union[str, IOBase, MutableSequence[Any], MutableMapping[str, Any]], *, unicode_escape: bool = True, uri_decode: bool = False, diff --git a/tests/test_issues.py b/tests/test_issues.py index 27bb615..c88f7f4 100644 --- a/tests/test_issues.py +++ b/tests/test_issues.py @@ -1,5 +1,7 @@ import pytest +from jsonpath import JSONPatch +from jsonpath import JSONPatchError from jsonpath import JSONPointerIndexError from jsonpath import findall from jsonpath import pointer @@ -100,3 +102,16 @@ def test_issue_115() -> None: # Negative index with pytest.raises(JSONPointerIndexError): pointer.resolve("/users/-1/score", data) + + +def test_issue_117() -> None: + # When the target value is an array of length 2, /foo/2 is the same as /foo/- + patch = JSONPatch().add(path="/foo/2", value=99) + data = {"foo": ["bar", "baz"]} + assert patch.apply(data) == {"foo": ["bar", "baz", 99]} + + # Array length + 1 raises + patch = JSONPatch().add(path="/foo/3", value=99) + data = {"foo": ["bar", "baz"]} + with pytest.raises(JSONPatchError): + patch.apply(data)