Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Instantiate correct [R]JP subclasses #105

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
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