Skip to content

Commit

Permalink
Restructure and document exceptions
Browse files Browse the repository at this point in the history
  • Loading branch information
marksparkza committed Jun 3, 2023
1 parent 445a4e7 commit 65314fa
Show file tree
Hide file tree
Showing 10 changed files with 125 additions and 80 deletions.
25 changes: 25 additions & 0 deletions CHANGELOG.rst
@@ -1,6 +1,31 @@
Changelog
=========

v0.11.0 (2023-06-03)
--------------------
Breaking changes:

* ``RelativeJSONPointerError`` superseded by ``RelativeJSONPointerMalformedError``
and ``RelativeJSONPointerReferenceError``

Deprecations:

* Exception classes dropped from the top-level package API
* ``jschon.exceptions`` module renamed to ``jschon.exc``

Documentation:

* Exception classes documented
* Custom keyword example refactored to raise exception in constructor

Miscellaneous:

* ``JschonError`` base exception class introduced
* ``JSONPointerError`` partitioned into ``JSONPointerMalformedError``,
``JSONPointerReferenceError``, ``RelativeJSONPointerMalformedError``
and ``RelativeJSONPointerReferenceError``


v0.10.3 (2023-05-21)
--------------------
Bug Fixes:
Expand Down
7 changes: 0 additions & 7 deletions docs/api.rst
Expand Up @@ -8,39 +8,32 @@ Catalog
.. autofunction:: jschon.create_catalog

* :class:`~jschon.catalog.Catalog`
* :class:`~jschon.exceptions.CatalogError`
* :class:`~jschon.catalog.LocalSource`
* :class:`~jschon.catalog.RemoteSource`

JSON
^^^^
* :class:`~jschon.json.JSON`
* :class:`~jschon.json.JSONCompatible`
* :class:`~jschon.exceptions.JSONError`

JSON Patch
^^^^^^^^^^
* :class:`~jschon.jsonpatch.JSONPatch`
* :class:`~jschon.exceptions.JSONPatchError`
* :class:`~jschon.jsonpatch.JSONPatchOperation`

JSON Pointer
^^^^^^^^^^^^
* :class:`~jschon.jsonpointer.JSONPointer`
* :class:`~jschon.exceptions.JSONPointerError`
* :class:`~jschon.jsonpointer.RelativeJSONPointer`
* :class:`~jschon.exceptions.RelativeJSONPointerError`

JSON Schema
^^^^^^^^^^^
* :class:`~jschon.jsonschema.JSONSchema`
* :class:`~jschon.exceptions.JSONSchemaError`
* :class:`~jschon.jsonschema.Result`

URI
^^^
* :class:`~jschon.uri.URI`
* :class:`~jschon.exceptions.URIError`

Module Reference
----------------
Expand Down
3 changes: 3 additions & 0 deletions docs/reference/exc.rst
@@ -0,0 +1,3 @@
jschon.exc
==========
.. automodule:: jschon.exc
3 changes: 0 additions & 3 deletions docs/reference/exceptions.rst

This file was deleted.

26 changes: 14 additions & 12 deletions examples/custom_keyword.py
Expand Up @@ -28,22 +28,24 @@ class EnumRefKeyword(Keyword):
# ignore non-string instances
instance_types = "string",

def __init__(self, parentschema: JSONSchema, value: str):
super().__init__(parentschema, value)

# raise an exception during schema construction if a reference is invalid
if value not in remote_enum_cache:
raise JSONSchemaError(f"Unknown remote enumeration {value}")

def evaluate(self, instance: JSON, result: Result) -> None:
# get the keyword's value as it appears in the JSON schema
enum_id = self.json.value
try:
# retrieve the enumeration from the remote enumeration cache
enum = remote_enum_cache[enum_id]
except KeyError:
raise JSONSchemaError(f"Unknown remote enumeration {enum_id}")

# test the value of the current JSON instance node against the enumeration
if instance.data in enum:
# the keyword's value is a reference to a remote enumeration
enum_ref = self.json.value

# evaluate the current JSON instance node against the enumeration
if instance.data in remote_enum_cache.get(enum_ref):
# (optionally) on success, annotate the result
result.annotate(enum_id)
result.annotate(enum_ref)
else:
# on failure, mark the result as failed, with an (optional) error message
result.fail(f"The instance is not a member of the {enum_id} enumeration")
result.fail(f"The instance is not a member of the {enum_ref} enumeration")


# initialize the catalog, with JSON Schema 2020-12 vocabulary support
Expand Down
11 changes: 2 additions & 9 deletions jschon/__init__.py
@@ -1,5 +1,5 @@
from .catalog import Catalog, LocalSource, RemoteSource
from .exceptions import CatalogError, JSONError, JSONPatchError, JSONPointerError, JSONSchemaError, RelativeJSONPointerError, URIError
from .exceptions import CatalogError, JSONError, JSONPatchError, JSONPointerError, JSONSchemaError, URIError
from .json import JSON, JSONCompatible
from .jsonpatch import JSONPatch, JSONPatchOperation
from .jsonpointer import JSONPointer, RelativeJSONPointer
Expand All @@ -8,28 +8,21 @@

__all__ = [
'Catalog',
'CatalogError',
'JSON',
'JSONCompatible',
'JSONError',
'JSONPatch',
'JSONPatchError',
'JSONPatchOperation',
'JSONPointer',
'JSONPointerError',
'JSONSchema',
'JSONSchemaError',
'LocalSource',
'RelativeJSONPointer',
'RelativeJSONPointerError',
'RemoteSource',
'Result',
'URI',
'URIError',
'create_catalog',
]

__version__ = '0.10.3'
__version__ = '0.11.0'


def create_catalog(*versions: str, name: str = 'catalog') -> Catalog:
Expand Down
50 changes: 50 additions & 0 deletions jschon/exc.py
@@ -0,0 +1,50 @@
class JschonError(Exception):
"""Generic error class."""


class CatalogError(JschonError):
"""An error originating in the :mod:`~jschon.catalog` module."""


class JSONError(JschonError):
"""An error originating in the :class:`~jschon.json` module."""


class JSONPatchError(JschonError):
"""An error originating in the :mod:`~jschon.jsonpatch` module."""


class JSONPointerError(JschonError):
"""An error originating in the :mod:`~jschon.jsonpointer` module."""


class JSONPointerMalformedError(JSONPointerError):
"""Raised for an invalid :class:`~jschon.jsonpointer.JSONPointer`
constructor argument."""


class JSONPointerReferenceError(JSONPointerError):
"""Raised when a :class:`~jschon.jsonpointer.JSONPointer`
evaluates a non-existent location in a document."""


class RelativeJSONPointerMalformedError(JSONPointerError):
"""Raised for an invalid :class:`~jschon.jsonpointer.RelativeJSONPointer`
constructor argument."""


class RelativeJSONPointerReferenceError(JSONPointerError):
"""Raised when a :class:`~jschon.jsonpointer.RelativeJSONPointer`
evaluates a non-existent location in a document."""


class JSONSchemaError(JschonError):
"""Raised when an error occurs during construction of a
:class:`~jschon.jsonschema.JSONSchema` object. May be raised by
:class:`~jschon.vocabulary.Keyword` initializers and reference
resolution methods.
"""


class URIError(JschonError):
"""An error originating in the :mod:`~jschon.uri` module."""
28 changes: 2 additions & 26 deletions jschon/exceptions.py
@@ -1,26 +1,2 @@
class CatalogError(Exception):
pass


class JSONError(Exception):
pass


class JSONPatchError(Exception):
pass


class JSONPointerError(Exception):
pass


class JSONSchemaError(Exception):
pass


class RelativeJSONPointerError(Exception):
pass


class URIError(Exception):
pass
# noinspection PyUnresolvedReferences
from .exc import *
37 changes: 21 additions & 16 deletions jschon/jsonpointer.py
Expand Up @@ -5,7 +5,12 @@
import urllib.parse
from typing import Any, Iterable, Literal, Mapping, Sequence, TYPE_CHECKING, Union, overload

from jschon.exceptions import JSONPointerError, RelativeJSONPointerError
from jschon.exc import (
JSONPointerMalformedError,
JSONPointerReferenceError,
RelativeJSONPointerMalformedError,
RelativeJSONPointerReferenceError,
)

if TYPE_CHECKING:
from jschon.json import JSON
Expand Down Expand Up @@ -74,16 +79,16 @@ def __new__(cls, *values: Union[str, Iterable[str]]) -> JSONPointer:
:param values: each value may either be an RFC 6901 string, or an iterable
of unescaped keys
:raise JSONPointerError: if a string argument does not conform to the RFC
6901 syntax
:raise JSONPointerMalformedError: if a string argument does not conform to
the RFC 6901 syntax
"""
self = object.__new__(cls)
self._keys = []

for value in values:
if isinstance(value, str):
if not JSONPointer._json_pointer_re.fullmatch(value):
raise JSONPointerError(f"'{value}' is not a valid JSON pointer")
raise JSONPointerMalformedError(f"'{value}' is not a valid JSON pointer")
self._keys.extend(self.unescape(token) for token in value.split('/')[1:])

elif isinstance(value, JSONPointer):
Expand Down Expand Up @@ -179,8 +184,8 @@ def evaluate(self, document: Any) -> Any:
will always fail.
:param document: any Python object
:raise JSONPointerError: if `self` references a non-existent location
within `document`
:raise JSONPointerReferenceError: if `self` references a non-existent
location in `document`
"""

def resolve(value, keys):
Expand All @@ -203,7 +208,7 @@ def resolve(value, keys):
except (KeyError, IndexError):
pass

raise JSONPointerError(f"Path '{self}' not found in document")
raise JSONPointerReferenceError(f"Path '{self}' not found in document")

return resolve(document, collections.deque(self._keys))

Expand Down Expand Up @@ -283,13 +288,13 @@ def __new__(
a value of 0, which is not allowed by the grammar, is treated as if
there is no adjustment.
:param ref: a :class:`JSONPointer` instance, or the literal ``'#'``
:raise RelativeJSONPointerError: for any invalid arguments
:raise RelativeJSONPointerMalformedError: for any invalid arguments
"""
self = object.__new__(cls)

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

up, over, ref = match.group('up', 'over', 'ref')
self.up = int(up)
Expand Down Expand Up @@ -337,25 +342,25 @@ def evaluate(self, document: JSON) -> Union[int, str, JSON]:
node = document
for _ in range(self.up):
if node.parent is None:
raise RelativeJSONPointerError('Up too many levels')
raise RelativeJSONPointerReferenceError('Up too many levels')
node = node.parent

if self.over:
if node.parent is None:
raise RelativeJSONPointerError('No containing node for index adjustment')
raise RelativeJSONPointerReferenceError('No containing node for index adjustment')
if node.parent.type != "array":
raise RelativeJSONPointerError(f'Index adjustment not valid for type {node.parent.type}')
raise RelativeJSONPointerReferenceError(f'Index adjustment not valid for type {node.parent.type}')
adjusted = int(node.key) + self.over
if adjusted < 0 or adjusted >= len(node.parent):
raise RelativeJSONPointerError(f'Index adjustment out of range')
raise RelativeJSONPointerReferenceError(f'Index adjustment out of range')
node = node.parent[adjusted]

if self.index:
if node.parent is None:
raise RelativeJSONPointerError('No containing node')
raise RelativeJSONPointerReferenceError('No containing node')
return int(node.key) if node.parent.type == "array" else node.key

try:
return self.path.evaluate(node)
except JSONPointerError as e:
raise RelativeJSONPointerError from e
except JSONPointerReferenceError as e:
raise RelativeJSONPointerReferenceError from e
15 changes: 8 additions & 7 deletions tests/test_jsonpointer.py
Expand Up @@ -6,7 +6,8 @@
import pytest
from hypothesis import given, strategies as hs

from jschon import JSON, JSONCompatible, JSONPointer, JSONPointerError, RelativeJSONPointer, RelativeJSONPointerError
from jschon import JSON, JSONCompatible, JSONPointer, RelativeJSONPointer
from jschon.exc import JSONPointerReferenceError, RelativeJSONPointerReferenceError
from jschon.utils import json_loadf
from tests.strategies import json, jsonpointer, jsonpointer_key, relative_jsonpointer, relative_jsonpointer_regex

Expand Down Expand Up @@ -85,18 +86,18 @@ def test_evaluate_jsonpointer(value, testkey):
assert JSONPointer(pointer).evaluate(JSON(value)) == target

if isinstance(value, list):
with pytest.raises(JSONPointerError):
with pytest.raises(JSONPointerReferenceError):
JSONPointer(f'/{len(value)}').evaluate(value)
with pytest.raises(JSONPointerError):
with pytest.raises(JSONPointerReferenceError):
JSONPointer('/-').evaluate(value)
with pytest.raises(JSONPointerError):
with pytest.raises(JSONPointerReferenceError):
JSONPointer('/').evaluate(value)
elif isinstance(value, dict):
if testkey not in value:
with pytest.raises(JSONPointerError):
with pytest.raises(JSONPointerReferenceError):
JSONPointer(f'/{jsonpointer_escape(testkey)}').evaluate(value)
else:
with pytest.raises(JSONPointerError):
with pytest.raises(JSONPointerReferenceError):
JSONPointer(f'/{value}').evaluate(value)


Expand Down Expand Up @@ -180,7 +181,7 @@ def test_evaluate_relative_jsonpointer(data, start, ref, result):
if result == '<data>':
result = data
elif result == '<fail>':
with pytest.raises(RelativeJSONPointerError):
with pytest.raises(RelativeJSONPointerReferenceError):
ref.evaluate(node)
return

Expand Down

0 comments on commit 65314fa

Please sign in to comment.