Skip to content

Commit 0c15a44

Browse files
committed
Refactor JSONPointer
1 parent 9d2fd15 commit 0c15a44

File tree

2 files changed

+82
-47
lines changed

2 files changed

+82
-47
lines changed

jsonpath/pointer.py

Lines changed: 71 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232

3333
class _Undefined:
3434
def __str__(self) -> str:
35-
return "<jsonpath.pointer.UNDEFINED>"
35+
return "<jsonpath.pointer.UNDEFINED>" # pragma: no cover
3636

3737

3838
UNDEFINED = _Undefined()
@@ -120,54 +120,79 @@ def _index(self, s: str) -> Union[str, int]:
120120
except ValueError:
121121
return s
122122

123-
def _getitem(self, obj: Any, key: Any) -> Any: # noqa: PLR0912
123+
def _getitem(self, obj: Any, key: Any) -> Any:
124124
try:
125+
# Handle the most common cases. A mapping with a string key, or a sequence
126+
# with an integer index.
127+
#
128+
# Note that `obj` does not have to be a Mapping or Sequence here. Any object
129+
# implementing `__getitem__` will do.
125130
return getitem(obj, key)
126131
except KeyError as err:
127-
# Try a string repr of the index-like item as a mapping key.
128-
if isinstance(key, int):
129-
try:
130-
return getitem(obj, str(key))
131-
except KeyError:
132-
raise JSONPointerKeyError(key) from err
133-
# Handle non-standard keys/property selector/pointer.
134-
if (
135-
isinstance(key, str)
136-
and isinstance(obj, Mapping)
137-
and key.startswith((self.keys_selector, "#"))
138-
and key[1:] in obj
139-
):
140-
return key[1:]
141-
# Handle non-standard index/property pointer (`#`)
142-
raise JSONPointerKeyError(key) from err
132+
return self._handle_key_error(obj, key, err)
143133
except TypeError as err:
144-
if isinstance(obj, Sequence) and not isinstance(obj, str):
145-
if key == "-":
146-
# "-" is a valid index when appending to a JSON array
147-
# with JSON Patch, but not when resolving a JSON Pointer.
148-
raise JSONPointerIndexError("index out of range") from None
149-
# Handle non-standard index pointer.
150-
if isinstance(key, str) and key.startswith("#"):
151-
_index = int(key[1:])
152-
if _index >= len(obj):
153-
raise JSONPointerIndexError(
154-
f"index out of range: {_index}"
155-
) from err
156-
return _index
157-
# Try int index. Reject non-zero ints that start with a zero.
158-
if isinstance(key, str):
159-
index = self._index(key)
160-
if isinstance(index, int):
161-
try:
162-
return getitem(obj, int(key))
163-
except IndexError as index_err:
164-
raise JSONPointerIndexError(
165-
f"index out of range: {key}"
166-
) from index_err
167-
raise JSONPointerTypeError(f"{key}: {err}") from err
134+
return self._handle_type_error(obj, key, err)
168135
except IndexError as err:
169136
raise JSONPointerIndexError(f"index out of range: {key}") from err
170137

138+
def _handle_key_error(self, obj: Any, key: Any, err: Exception) -> object:
139+
if isinstance(key, int):
140+
# Try a string repr of the index-like item as a mapping key.
141+
return self._getitem(obj, str(key))
142+
143+
# Handle non-standard keys/property selector/pointer.
144+
#
145+
# For the benefit of `RelativeJSONPointer.to()`, treat keys starting with a `#`
146+
# as a "key pointer". If `key[1:]` is a key in `obj`, return the key.
147+
#
148+
# Note that is a key with a leading `#` exists in `obj`, it will have been
149+
# handled by `_getitem`.
150+
#
151+
# TODO: Same goes for `~`
152+
if (
153+
isinstance(key, str)
154+
and isinstance(obj, Mapping)
155+
and key.startswith((self.keys_selector, "#"))
156+
and key[1:] in obj
157+
):
158+
return key[1:]
159+
160+
raise JSONPointerKeyError(key) from err
161+
162+
def _handle_type_error(self, obj: Any, key: Any, err: Exception) -> object:
163+
if (
164+
isinstance(obj, str)
165+
or not isinstance(obj, Sequence)
166+
or not isinstance(key, str)
167+
):
168+
raise JSONPointerTypeError(f"{key}: {err}") from err
169+
170+
# `obj` is array-like
171+
# `key` is a string
172+
173+
if key == "-":
174+
# "-" is a valid index when appending to a JSON array with JSON Patch, but
175+
# not when resolving a JSON Pointer.
176+
raise JSONPointerIndexError("index out of range") from None
177+
178+
# Handle non-standard index pointer.
179+
#
180+
# For the benefit of `RelativeJSONPointer.to()`, treat keys starting with a `#`
181+
# and followed by a valid index as an "index pointer". If `int(key[1:])` is
182+
# less than `len(obj)`, return the index.
183+
if re.match(r"#[1-9]\d*", key):
184+
_index = int(key[1:])
185+
if _index >= len(obj):
186+
raise JSONPointerIndexError(f"index out of range: {_index}") from err
187+
return _index
188+
189+
# Try int index. Reject non-zero ints that start with a zero.
190+
index = self._index(key)
191+
if isinstance(index, int):
192+
return self._getitem(obj, index)
193+
194+
raise JSONPointerTypeError(f"{key}: {err}") from err
195+
171196
def resolve(
172197
self,
173198
data: Union[str, IOBase, Sequence[object], Mapping[str, object]],
@@ -263,7 +288,7 @@ def from_match(
263288
pointer = cls._encode(match.parts)
264289
else:
265290
# This should not happen, unless the JSONPathMatch has been tampered with.
266-
pointer = ""
291+
pointer = "" # pragma: no cover
267292

268293
return cls(
269294
pointer,
@@ -328,10 +353,10 @@ def __eq__(self, other: object) -> bool:
328353
return isinstance(other, JSONPointer) and self.parts == other.parts
329354

330355
def __hash__(self) -> int:
331-
return hash(self.parts)
356+
return hash(self.parts) # pragma: no cover
332357

333358
def __repr__(self) -> str:
334-
return f"JSONPointer({self._s!r})"
359+
return f"JSONPointer({self._s!r})" # pragma: no cover
335360

336361
def exists(
337362
self, data: Union[str, IOBase, Sequence[object], Mapping[str, object]]
@@ -486,7 +511,7 @@ def __eq__(self, __value: object) -> bool:
486511
return isinstance(__value, RelativeJSONPointer) and str(self) == str(__value)
487512

488513
def __hash__(self) -> int:
489-
return hash((self.origin, self.index, self.pointer))
514+
return hash((self.origin, self.index, self.pointer)) # pragma: no cover
490515

491516
def _parse(
492517
self,

tests/test_json_pointer.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""JSONPointer test cases."""
2+
23
from io import StringIO
34
from typing import List
45
from typing import Union
@@ -261,7 +262,7 @@ def test_join_pointers_with_slash() -> None:
261262
assert str(pointer / "/bar") == "/bar"
262263

263264
with pytest.raises(TypeError):
264-
pointer / 0
265+
pointer / 0 # type: ignore
265266

266267

267268
def test_join_pointers() -> None:
@@ -299,6 +300,15 @@ def test_non_standard_index_pointer() -> None:
299300
JSONPointer("/foo/bar/#9").resolve(data)
300301

301302

303+
def test_non_standard_index_pointer_with_leading_zero() -> None:
304+
data = {"foo": {"bar": [1, 2, 3], "#baz": "hello"}}
305+
with pytest.raises(JSONPointerTypeError):
306+
JSONPointer("/foo/bar/#01").resolve(data)
307+
308+
with pytest.raises(JSONPointerTypeError):
309+
JSONPointer("/foo/bar/#09").resolve(data)
310+
311+
302312
def test_trailing_slash() -> None:
303313
data = {"foo": {"": [1, 2, 3], " ": [4, 5, 6]}}
304314
assert JSONPointer("/foo/").resolve(data) == [1, 2, 3]

0 commit comments

Comments
 (0)