Skip to content

Commit

Permalink
Instantiate correct [R]JP subclasses
Browse files Browse the repository at this point in the history
Always return an instance of the subclass where appropraite.
For RelativeJSONPointer, add a class property for the matching
JSONPointer subclass.
  • Loading branch information
handrews committed Aug 7, 2023
1 parent e6f71d0 commit c4b691f
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 30 deletions.
7 changes: 5 additions & 2 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ v0.11.1 (in development)
Features:

* Allow adding a source with a base URI of ``None`` to match full URIs as the ``relative_path``
* ``JSONPointer`` and ``RelativeJSONPointer`` now have class attributes defining
the exceptions that they use, which can be overidden in subclasses
* ``JSONPointer``, ``RelativeJSONPointer`` and their related exceptions can
now be subclassed together; the base classes will instantiate subclasses
where appropriate, and class attributes can be set to ensure that subclasses
will use subclassed exceptions, and a subclassed ``RelativeJSONPointer`` will
use its companion subclass of ``JSONPointer``
* Cached properties for accessing document and resource root schemas from subschemas


Expand Down
32 changes: 21 additions & 11 deletions jschon/jsonpointer.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def __getitem__(self, index):
if isinstance(index, int):
return self._keys[index]
if isinstance(index, slice):
return JSONPointer(self._keys[index])
return self.__class__(self._keys[index])
raise TypeError("Expecting int or slice")

def __len__(self) -> int:
Expand All @@ -141,9 +141,9 @@ def __truediv__(self, suffix: Iterable[str]) -> JSONPointer:
def __truediv__(self, suffix) -> JSONPointer:
"""Return `self / suffix`."""
if isinstance(suffix, str):
return JSONPointer(self, (suffix,))
return self.__class__(self, (suffix,))
if isinstance(suffix, Iterable):
return JSONPointer(self, suffix)
return self.__class__(self, suffix)
return NotImplemented

def __eq__(self, other: JSONPointer) -> bool:
Expand Down Expand Up @@ -182,7 +182,7 @@ def __str__(self) -> str:

def __repr__(self) -> str:
"""Return `repr(self)`."""
return f"JSONPointer({str(self)!r})"
return f"{self.__class__.__name__}({str(self)!r})"

def evaluate(self, document: Any) -> Any:
"""Return the value within `document` at the location referenced by `self`.
Expand Down Expand Up @@ -210,7 +210,7 @@ def resolve(value, keys):
if isjson and value.type == "array" or \
not isjson and isinstance(value, Sequence) and \
not isinstance(value, str) and \
JSONPointer._array_index_re.fullmatch(key):
self._array_index_re.fullmatch(key):
return resolve(value[int(key)], keys)

except (KeyError, IndexError):
Expand All @@ -230,7 +230,7 @@ def parse_uri_fragment(cls, value: str) -> JSONPointer:
:param value: a percent-encoded URI fragment
"""
return JSONPointer(urllib.parse.unquote(value))
return cls(urllib.parse.unquote(value))

def uri_fragment(self) -> str:
"""Return a percent-encoded URI fragment representation of `self`.
Expand Down Expand Up @@ -283,6 +283,9 @@ class RelativeJSONPointer:
] = RelativeJSONPointerReferenceError
"""Exception raised when the Relative JSON Pointer cannot be resolved against a document."""

json_pointer_class: Type[JSONPointer] = JSONPointer
"""JSONPointer subclass used to implement the ``ref`` portion of the relative pointer."""

_regex = re.compile(RELATIVE_JSON_POINTER_RE)

def __new__(
Expand All @@ -292,7 +295,7 @@ def __new__(
*,
up: int = 0,
over: int = 0,
ref: Union[JSONPointer, Literal['#']] = JSONPointer(),
ref: Union[JSONPointer, Literal['#'], Literal['']] = '',
) -> RelativeJSONPointer:
"""Create and return a new :class:`RelativeJSONPointer` instance.
Expand All @@ -310,18 +313,25 @@ def __new__(
self = object.__new__(cls)

if value is not None:
if not (match := RelativeJSONPointer._regex.fullmatch(value)):
if not (match := cls._regex.fullmatch(value)):
raise cls.malformed_exc(f"'{value}' is not a valid relative JSON pointer")

up, over, ref = match.group('up', 'over', 'ref')
self.up = int(up)
self.over = int(over) if over else 0
self.path = JSONPointer(ref) if ref != '#' else None
self.path = cls.json_pointer_class(ref) if ref != '#' else None

else:
self.up = up
self.over = over
self.path = ref if isinstance(ref, JSONPointer) else None
if isinstance(ref, cls.json_pointer_class):
self.path = ref
elif ref == '':
self.path = cls.json_pointer_class()
elif isinstance(ref, JSONPointer):
self.path = cls.json_pointer_class(str(ref))
else:
self.path = None

self.index = ref == '#'

Expand Down Expand Up @@ -353,7 +363,7 @@ def __str__(self) -> str:

def __repr__(self) -> str:
"""Return `repr(self)`."""
return f'RelativeJSONPointer({str(self)!r})'
return f'{self.__class__.__name__}({str(self)!r})'

def evaluate(self, document: JSON) -> Union[int, str, JSON]:
"""Return the value within `document` at the location referenced by `self`.
Expand Down
44 changes: 27 additions & 17 deletions tests/test_jsonpointer.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pathlib
import re
from copy import copy
from typing import Dict, List, Union
from typing import Dict, List, Union, Type

import pytest
from hypothesis import example, given, strategies as hs
Expand Down Expand Up @@ -44,6 +44,7 @@ class RJPRefError(RelativeJSONPointerReferenceError):
class RJPtr(RelativeJSONPointer):
malformed_exc = RJPMalError
reference_exc = RJPRefError
json_pointer_class = JPtr


##################### End subclasses #########################
Expand Down Expand Up @@ -71,19 +72,20 @@ def jsonpointer_unescape(token: str):
return token.replace('~1', '/').replace('~0', '~')


@pytest.mark.parametrize('jp_cls', (JSONPointer, JPtr))
@given(hs.lists(jsonpointer | hs.lists(jsonpointer_key)))
def test_create_jsonpointer(values: List[Union[str, List[str]]]):
def test_create_jsonpointer(jp_cls: Type[JSONPointer], values: List[Union[str, List[str]]]):
keys = []
for value in values:
keys += [jsonpointer_unescape(token) for token in value.split('/')[1:]] if isinstance(value, str) else value

ptr0 = JSONPointer(*values)
assert ptr0 == (ptr1 := JSONPointer(*values))
assert ptr0 == (ptr2 := JSONPointer(keys))
assert ptr0 == (ptr3 := JSONPointer(ptr0))
assert ptr0 != (ptr4 := JSONPointer() if keys else JSONPointer('/'))
assert ptr0 != (ptr5 := JSONPointer('/', keys))
assert JSONPointer(ptr0, keys, *values) == JSONPointer(*values, keys, ptr0)
ptr0 = jp_cls(*values)
assert ptr0 == (ptr1 := jp_cls(*values))
assert ptr0 == (ptr2 := jp_cls(keys))
assert ptr0 == (ptr3 := jp_cls(ptr0))
assert ptr0 != (ptr4 := jp_cls() if keys else jp_cls('/'))
assert ptr0 != (ptr5 := jp_cls('/', keys))
assert jp_cls(ptr0, keys, *values) == jp_cls(*values, keys, ptr0)

ptrs = {ptr0, ptr1, ptr2, ptr3}
assert ptrs == {ptr0}
Expand All @@ -103,17 +105,21 @@ def test_malformed_jsonpointer(jp_cls):
assert exc_info.type == jp_cls.malformed_exc


@pytest.mark.parametrize('jp_cls', (JSONPointer, JPtr))
@given(jsonpointer, jsonpointer_key)
def test_extend_jsonpointer_one_key(value, newkey):
pointer = JSONPointer(value) / newkey
def test_extend_jsonpointer_one_key(jp_cls, value, newkey):
pointer = jp_cls(value) / newkey
newtoken = jsonpointer_escape(newkey)
assert type(pointer) == jp_cls
assert pointer[-1] == newkey
assert str(pointer) == f'{value}/{newtoken}'


@pytest.mark.parametrize('jp_cls', (JSONPointer, JPtr))
@given(jsonpointer, hs.lists(jsonpointer_key))
def test_extend_jsonpointer_multi_keys(value, newkeys):
pointer = (base_ptr := JSONPointer(value)) / newkeys
def test_extend_jsonpointer_multi_keys(jp_cls, value, newkeys):
pointer = (base_ptr := jp_cls(value)) / newkeys
assert type(pointer) == jp_cls
for i in range(len(newkeys)):
assert pointer[len(base_ptr) + i] == newkeys[i]
assert str(pointer) == value + ''.join(f'/{jsonpointer_escape(key)}' for key in newkeys)
Expand Down Expand Up @@ -160,7 +166,7 @@ def test_evaluate_jsonpointer(jp_cls, value, testkey):

@given(jsonpointer, jsonpointer)
def test_compare_jsonpointer(value1, value2):
ptr1 = JSONPointer(value1)
ptr1 = JPtr(value1)
ptr2 = JSONPointer(value2)
assert ptr1 == JSONPointer(ptr1)
assert ptr1 <= JSONPointer(ptr1)
Expand All @@ -179,8 +185,9 @@ def test_compare_jsonpointer(value1, value2):
assert (ptr1 > ptr2) == (ptr1 >= ptr2 and ptr1 != ptr2)


@pytest.mark.parametrize('rjp_cls', (RelativeJSONPointer, RJPtr))
@given(relative_jsonpointer)
def test_create_relative_jsonpointer(value):
def test_create_relative_jsonpointer(rjp_cls, value):
match = re.fullmatch(relative_jsonpointer_regex, value)
up, over, ref = match.group('up', 'over', 'ref')
kwargs = dict(
Expand All @@ -189,11 +196,14 @@ def test_create_relative_jsonpointer(value):
ref=JSONPointer(ref) if ref != '#' else ref,
)

r1 = RelativeJSONPointer(value)
r2 = RelativeJSONPointer(**kwargs)
r1 = rjp_cls(value)
r2 = rjp_cls(**kwargs)
assert r1 == r2
assert str(r1) == value
assert eval(repr(r1)) == r1
if type(kwargs['ref']) == JSONPointer:
assert type(r1.path) == rjp_cls.json_pointer_class
assert type(r2.path) == rjp_cls.json_pointer_class

oldkwargs = copy(kwargs)
if up == '0':
Expand Down

0 comments on commit c4b691f

Please sign in to comment.