Skip to content

Commit 65314fa

Browse files
committed
Restructure and document exceptions
1 parent 445a4e7 commit 65314fa

File tree

10 files changed

+125
-80
lines changed

10 files changed

+125
-80
lines changed

CHANGELOG.rst

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,31 @@
11
Changelog
22
=========
33

4+
v0.11.0 (2023-06-03)
5+
--------------------
6+
Breaking changes:
7+
8+
* ``RelativeJSONPointerError`` superseded by ``RelativeJSONPointerMalformedError``
9+
and ``RelativeJSONPointerReferenceError``
10+
11+
Deprecations:
12+
13+
* Exception classes dropped from the top-level package API
14+
* ``jschon.exceptions`` module renamed to ``jschon.exc``
15+
16+
Documentation:
17+
18+
* Exception classes documented
19+
* Custom keyword example refactored to raise exception in constructor
20+
21+
Miscellaneous:
22+
23+
* ``JschonError`` base exception class introduced
24+
* ``JSONPointerError`` partitioned into ``JSONPointerMalformedError``,
25+
``JSONPointerReferenceError``, ``RelativeJSONPointerMalformedError``
26+
and ``RelativeJSONPointerReferenceError``
27+
28+
429
v0.10.3 (2023-05-21)
530
--------------------
631
Bug Fixes:

docs/api.rst

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,39 +8,32 @@ Catalog
88
.. autofunction:: jschon.create_catalog
99

1010
* :class:`~jschon.catalog.Catalog`
11-
* :class:`~jschon.exceptions.CatalogError`
1211
* :class:`~jschon.catalog.LocalSource`
1312
* :class:`~jschon.catalog.RemoteSource`
1413

1514
JSON
1615
^^^^
1716
* :class:`~jschon.json.JSON`
1817
* :class:`~jschon.json.JSONCompatible`
19-
* :class:`~jschon.exceptions.JSONError`
2018

2119
JSON Patch
2220
^^^^^^^^^^
2321
* :class:`~jschon.jsonpatch.JSONPatch`
24-
* :class:`~jschon.exceptions.JSONPatchError`
2522
* :class:`~jschon.jsonpatch.JSONPatchOperation`
2623

2724
JSON Pointer
2825
^^^^^^^^^^^^
2926
* :class:`~jschon.jsonpointer.JSONPointer`
30-
* :class:`~jschon.exceptions.JSONPointerError`
3127
* :class:`~jschon.jsonpointer.RelativeJSONPointer`
32-
* :class:`~jschon.exceptions.RelativeJSONPointerError`
3328

3429
JSON Schema
3530
^^^^^^^^^^^
3631
* :class:`~jschon.jsonschema.JSONSchema`
37-
* :class:`~jschon.exceptions.JSONSchemaError`
3832
* :class:`~jschon.jsonschema.Result`
3933

4034
URI
4135
^^^
4236
* :class:`~jschon.uri.URI`
43-
* :class:`~jschon.exceptions.URIError`
4437

4538
Module Reference
4639
----------------

docs/reference/exc.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
jschon.exc
2+
==========
3+
.. automodule:: jschon.exc

docs/reference/exceptions.rst

Lines changed: 0 additions & 3 deletions
This file was deleted.

examples/custom_keyword.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,24 @@ class EnumRefKeyword(Keyword):
2828
# ignore non-string instances
2929
instance_types = "string",
3030

31+
def __init__(self, parentschema: JSONSchema, value: str):
32+
super().__init__(parentschema, value)
33+
34+
# raise an exception during schema construction if a reference is invalid
35+
if value not in remote_enum_cache:
36+
raise JSONSchemaError(f"Unknown remote enumeration {value}")
37+
3138
def evaluate(self, instance: JSON, result: Result) -> None:
32-
# get the keyword's value as it appears in the JSON schema
33-
enum_id = self.json.value
34-
try:
35-
# retrieve the enumeration from the remote enumeration cache
36-
enum = remote_enum_cache[enum_id]
37-
except KeyError:
38-
raise JSONSchemaError(f"Unknown remote enumeration {enum_id}")
39-
40-
# test the value of the current JSON instance node against the enumeration
41-
if instance.data in enum:
39+
# the keyword's value is a reference to a remote enumeration
40+
enum_ref = self.json.value
41+
42+
# evaluate the current JSON instance node against the enumeration
43+
if instance.data in remote_enum_cache.get(enum_ref):
4244
# (optionally) on success, annotate the result
43-
result.annotate(enum_id)
45+
result.annotate(enum_ref)
4446
else:
4547
# on failure, mark the result as failed, with an (optional) error message
46-
result.fail(f"The instance is not a member of the {enum_id} enumeration")
48+
result.fail(f"The instance is not a member of the {enum_ref} enumeration")
4749

4850

4951
# initialize the catalog, with JSON Schema 2020-12 vocabulary support

jschon/__init__.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from .catalog import Catalog, LocalSource, RemoteSource
2-
from .exceptions import CatalogError, JSONError, JSONPatchError, JSONPointerError, JSONSchemaError, RelativeJSONPointerError, URIError
2+
from .exceptions import CatalogError, JSONError, JSONPatchError, JSONPointerError, JSONSchemaError, URIError
33
from .json import JSON, JSONCompatible
44
from .jsonpatch import JSONPatch, JSONPatchOperation
55
from .jsonpointer import JSONPointer, RelativeJSONPointer
@@ -8,28 +8,21 @@
88

99
__all__ = [
1010
'Catalog',
11-
'CatalogError',
1211
'JSON',
1312
'JSONCompatible',
14-
'JSONError',
1513
'JSONPatch',
16-
'JSONPatchError',
1714
'JSONPatchOperation',
1815
'JSONPointer',
19-
'JSONPointerError',
2016
'JSONSchema',
21-
'JSONSchemaError',
2217
'LocalSource',
2318
'RelativeJSONPointer',
24-
'RelativeJSONPointerError',
2519
'RemoteSource',
2620
'Result',
2721
'URI',
28-
'URIError',
2922
'create_catalog',
3023
]
3124

32-
__version__ = '0.10.3'
25+
__version__ = '0.11.0'
3326

3427

3528
def create_catalog(*versions: str, name: str = 'catalog') -> Catalog:

jschon/exc.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
class JschonError(Exception):
2+
"""Generic error class."""
3+
4+
5+
class CatalogError(JschonError):
6+
"""An error originating in the :mod:`~jschon.catalog` module."""
7+
8+
9+
class JSONError(JschonError):
10+
"""An error originating in the :class:`~jschon.json` module."""
11+
12+
13+
class JSONPatchError(JschonError):
14+
"""An error originating in the :mod:`~jschon.jsonpatch` module."""
15+
16+
17+
class JSONPointerError(JschonError):
18+
"""An error originating in the :mod:`~jschon.jsonpointer` module."""
19+
20+
21+
class JSONPointerMalformedError(JSONPointerError):
22+
"""Raised for an invalid :class:`~jschon.jsonpointer.JSONPointer`
23+
constructor argument."""
24+
25+
26+
class JSONPointerReferenceError(JSONPointerError):
27+
"""Raised when a :class:`~jschon.jsonpointer.JSONPointer`
28+
evaluates a non-existent location in a document."""
29+
30+
31+
class RelativeJSONPointerMalformedError(JSONPointerError):
32+
"""Raised for an invalid :class:`~jschon.jsonpointer.RelativeJSONPointer`
33+
constructor argument."""
34+
35+
36+
class RelativeJSONPointerReferenceError(JSONPointerError):
37+
"""Raised when a :class:`~jschon.jsonpointer.RelativeJSONPointer`
38+
evaluates a non-existent location in a document."""
39+
40+
41+
class JSONSchemaError(JschonError):
42+
"""Raised when an error occurs during construction of a
43+
:class:`~jschon.jsonschema.JSONSchema` object. May be raised by
44+
:class:`~jschon.vocabulary.Keyword` initializers and reference
45+
resolution methods.
46+
"""
47+
48+
49+
class URIError(JschonError):
50+
"""An error originating in the :mod:`~jschon.uri` module."""

jschon/exceptions.py

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,2 @@
1-
class CatalogError(Exception):
2-
pass
3-
4-
5-
class JSONError(Exception):
6-
pass
7-
8-
9-
class JSONPatchError(Exception):
10-
pass
11-
12-
13-
class JSONPointerError(Exception):
14-
pass
15-
16-
17-
class JSONSchemaError(Exception):
18-
pass
19-
20-
21-
class RelativeJSONPointerError(Exception):
22-
pass
23-
24-
25-
class URIError(Exception):
26-
pass
1+
# noinspection PyUnresolvedReferences
2+
from .exc import *

jschon/jsonpointer.py

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
import urllib.parse
66
from typing import Any, Iterable, Literal, Mapping, Sequence, TYPE_CHECKING, Union, overload
77

8-
from jschon.exceptions import JSONPointerError, RelativeJSONPointerError
8+
from jschon.exc import (
9+
JSONPointerMalformedError,
10+
JSONPointerReferenceError,
11+
RelativeJSONPointerMalformedError,
12+
RelativeJSONPointerReferenceError,
13+
)
914

1015
if TYPE_CHECKING:
1116
from jschon.json import JSON
@@ -74,16 +79,16 @@ def __new__(cls, *values: Union[str, Iterable[str]]) -> JSONPointer:
7479
7580
:param values: each value may either be an RFC 6901 string, or an iterable
7681
of unescaped keys
77-
:raise JSONPointerError: if a string argument does not conform to the RFC
78-
6901 syntax
82+
:raise JSONPointerMalformedError: if a string argument does not conform to
83+
the RFC 6901 syntax
7984
"""
8085
self = object.__new__(cls)
8186
self._keys = []
8287

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

8994
elif isinstance(value, JSONPointer):
@@ -179,8 +184,8 @@ def evaluate(self, document: Any) -> Any:
179184
will always fail.
180185
181186
:param document: any Python object
182-
:raise JSONPointerError: if `self` references a non-existent location
183-
within `document`
187+
:raise JSONPointerReferenceError: if `self` references a non-existent
188+
location in `document`
184189
"""
185190

186191
def resolve(value, keys):
@@ -203,7 +208,7 @@ def resolve(value, keys):
203208
except (KeyError, IndexError):
204209
pass
205210

206-
raise JSONPointerError(f"Path '{self}' not found in document")
211+
raise JSONPointerReferenceError(f"Path '{self}' not found in document")
207212

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

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

290295
if value is not None:
291296
if not (match := RelativeJSONPointer._regex.fullmatch(value)):
292-
raise RelativeJSONPointerError(f"'{value}' is not a valid relative JSON pointer")
297+
raise RelativeJSONPointerMalformedError(f"'{value}' is not a valid relative JSON pointer")
293298

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

343348
if self.over:
344349
if node.parent is None:
345-
raise RelativeJSONPointerError('No containing node for index adjustment')
350+
raise RelativeJSONPointerReferenceError('No containing node for index adjustment')
346351
if node.parent.type != "array":
347-
raise RelativeJSONPointerError(f'Index adjustment not valid for type {node.parent.type}')
352+
raise RelativeJSONPointerReferenceError(f'Index adjustment not valid for type {node.parent.type}')
348353
adjusted = int(node.key) + self.over
349354
if adjusted < 0 or adjusted >= len(node.parent):
350-
raise RelativeJSONPointerError(f'Index adjustment out of range')
355+
raise RelativeJSONPointerReferenceError(f'Index adjustment out of range')
351356
node = node.parent[adjusted]
352357

353358
if self.index:
354359
if node.parent is None:
355-
raise RelativeJSONPointerError('No containing node')
360+
raise RelativeJSONPointerReferenceError('No containing node')
356361
return int(node.key) if node.parent.type == "array" else node.key
357362

358363
try:
359364
return self.path.evaluate(node)
360-
except JSONPointerError as e:
361-
raise RelativeJSONPointerError from e
365+
except JSONPointerReferenceError as e:
366+
raise RelativeJSONPointerReferenceError from e

tests/test_jsonpointer.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
import pytest
77
from hypothesis import given, strategies as hs
88

9-
from jschon import JSON, JSONCompatible, JSONPointer, JSONPointerError, RelativeJSONPointer, RelativeJSONPointerError
9+
from jschon import JSON, JSONCompatible, JSONPointer, RelativeJSONPointer
10+
from jschon.exc import JSONPointerReferenceError, RelativeJSONPointerReferenceError
1011
from jschon.utils import json_loadf
1112
from tests.strategies import json, jsonpointer, jsonpointer_key, relative_jsonpointer, relative_jsonpointer_regex
1213

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

8788
if isinstance(value, list):
88-
with pytest.raises(JSONPointerError):
89+
with pytest.raises(JSONPointerReferenceError):
8990
JSONPointer(f'/{len(value)}').evaluate(value)
90-
with pytest.raises(JSONPointerError):
91+
with pytest.raises(JSONPointerReferenceError):
9192
JSONPointer('/-').evaluate(value)
92-
with pytest.raises(JSONPointerError):
93+
with pytest.raises(JSONPointerReferenceError):
9394
JSONPointer('/').evaluate(value)
9495
elif isinstance(value, dict):
9596
if testkey not in value:
96-
with pytest.raises(JSONPointerError):
97+
with pytest.raises(JSONPointerReferenceError):
9798
JSONPointer(f'/{jsonpointer_escape(testkey)}').evaluate(value)
9899
else:
99-
with pytest.raises(JSONPointerError):
100+
with pytest.raises(JSONPointerReferenceError):
100101
JSONPointer(f'/{value}').evaluate(value)
101102

102103

@@ -180,7 +181,7 @@ def test_evaluate_relative_jsonpointer(data, start, ref, result):
180181
if result == '<data>':
181182
result = data
182183
elif result == '<fail>':
183-
with pytest.raises(RelativeJSONPointerError):
184+
with pytest.raises(RelativeJSONPointerReferenceError):
184185
ref.evaluate(node)
185186
return
186187

0 commit comments

Comments
 (0)