Skip to content

Commit c6484dd

Browse files
committed
Fix ErrorTree polluting contents on index access
ErrorTree used a defaultdict for _contents, which meant accessing a non-existent index via __getitem__ would silently create an empty entry. This caused __iter__ and __contains__ to return incorrect results after accessing an index with no errors. Replaced defaultdict with a regular dict and changed __init__ to explicitly create subtrees. __getitem__ now returns a new empty ErrorTree for valid indices without storing it in _contents.
1 parent 5a5fa97 commit c6484dd

3 files changed

Lines changed: 35 additions & 4 deletions

File tree

commit_msg.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Fix ErrorTree polluting contents on index access
2+
3+
ErrorTree used a defaultdict for _contents, which meant accessing
4+
a non-existent index via __getitem__ would silently create an empty
5+
entry. This caused __iter__ and __contains__ to return incorrect
6+
results after accessing an index with no errors.
7+
8+
Replaced defaultdict with a regular dict and changed __init__ to
9+
explicitly create subtrees. __getitem__ now returns a new empty
10+
ErrorTree for valid indices without storing it in _contents.

jsonschema/exceptions.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"""
44
from __future__ import annotations
55

6-
from collections import defaultdict, deque
6+
from collections import deque
77
from pprint import pformat
88
from textwrap import dedent, indent
99
from typing import TYPE_CHECKING, Any, ClassVar
@@ -321,12 +321,14 @@ class ErrorTree:
321321

322322
def __init__(self, errors: Iterable[ValidationError] = ()):
323323
self.errors: MutableMapping[str, ValidationError] = {}
324-
self._contents: Mapping[str, ErrorTree] = defaultdict(self.__class__)
324+
self._contents: dict[str | int, ErrorTree] = {}
325325

326326
for error in errors:
327327
container = self
328328
for element in error.path:
329-
container = container[element]
329+
if element not in container._contents:
330+
container._contents[element] = self.__class__()
331+
container = container._contents[element]
330332
container.errors[error.validator] = error
331333

332334
container._instance = error.instance
@@ -348,7 +350,9 @@ def __getitem__(self, index):
348350
"""
349351
if self._instance is not _unset and index not in self:
350352
self._instance[index]
351-
return self._contents[index]
353+
if index in self._contents:
354+
return self._contents[index]
355+
return self.__class__()
352356

353357
def __setitem__(self, index: str | int, value: ErrorTree):
354358
"""

jsonschema/tests/test_exceptions.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,23 @@ def test_repr_empty(self):
506506
tree = exceptions.ErrorTree([])
507507
self.assertEqual(repr(tree), "<ErrorTree (0 total errors)>")
508508

509+
def test_accessing_index_without_error_does_not_pollute_contents(self):
510+
error = exceptions.ValidationError(
511+
"not a number",
512+
validator="type",
513+
path=[0],
514+
instance=["spam", 2],
515+
)
516+
tree = exceptions.ErrorTree([error])
517+
self.assertEqual(list(tree), [0])
518+
self.assertNotIn(1, tree)
519+
520+
# accessing an index with no error should not add it to the tree
521+
tree[1]
522+
523+
self.assertEqual(list(tree), [0])
524+
self.assertNotIn(1, tree)
525+
509526

510527
class TestErrorInitReprStr(TestCase):
511528
def make_error(self, **kwargs):

0 commit comments

Comments
 (0)