Skip to content
Closed
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
20 changes: 16 additions & 4 deletions jsonschema/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""
from __future__ import annotations

from collections import defaultdict, deque
from collections import deque
from pprint import pformat
from textwrap import dedent, indent
from typing import TYPE_CHECKING, Any, ClassVar
Expand Down Expand Up @@ -321,12 +321,18 @@ class ErrorTree:

def __init__(self, errors: Iterable[ValidationError] = ()):
self.errors: MutableMapping[str, ValidationError] = {}
self._contents: Mapping[str, ErrorTree] = defaultdict(self.__class__)
self._contents: MutableMapping[str, ErrorTree] = {}

for error in errors:
container = self
for element in error.path:
container = container[element]
# Populate (and persist) intermediate nodes directly,
# bypassing __getitem__ -- which, for public callers,
# must *not* have the side effect of adding an index to
# the tree merely by reading it. See GH #1328.
container = container._contents.setdefault(
element, self.__class__(),
)
container.errors[error.validator] = error

container._instance = error.instance
Expand All @@ -345,10 +351,16 @@ def __getitem__(self, index):
to and is not known by this tree, whatever error would be raised
by ``instance.__getitem__`` will be propagated (usually this is
some subclass of `LookupError`.

Note that unlike a normal mapping, accessing an index which has
no known errors does not add that index to the tree -- it
simply returns a new, empty `ErrorTree`. Use `in` (see
`ErrorTree.__contains__`) or `iter` (see `ErrorTree.__iter__`)
to check which indices are actually known to have errors.
"""
if self._instance is not _unset and index not in self:
self._instance[index]
return self._contents[index]
return self._contents.get(index, self.__class__())

def __setitem__(self, index: str | int, value: ErrorTree):
"""
Expand Down
20 changes: 20 additions & 0 deletions jsonschema/tests/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,26 @@ def test_it_does_not_contain_an_item_if_the_item_had_no_error(self):
tree = exceptions.ErrorTree(errors)
self.assertNotIn("foo", tree)

def test_accessing_an_index_with_no_error_does_not_add_it(self):
"""
Regression test for
https://github.com/python-jsonschema/jsonschema/issues/1328

Merely reading ``tree[index]`` for an index with no error must
not have the side effect of making that index appear to have
one on subsequent ``__contains__`` / ``__iter__`` calls.
"""
errors = [exceptions.ValidationError("a message", path=["bar"])]
tree = exceptions.ErrorTree(errors)

self.assertNotIn("foo", tree)
self.assertEqual(list(tree), ["bar"])

tree["foo"] # merely accessing it should not add it

self.assertNotIn("foo", tree)
self.assertEqual(list(tree), ["bar"])

def test_keywords_that_failed_appear_in_errors_dict(self):
error = exceptions.ValidationError("a message", validator="foo")
tree = exceptions.ErrorTree([error])
Expand Down
Loading