BaseDict._get_dict_or_value() recursively unwraps nested benedict values with no cycle detection, so assigning a benedict into its own nested descendant a second time recurses forever and crashes with RecursionError. Track ids of mappings currently being unwrapped and raise a clear ValueError as soon as a self-reference is detected, instead of recursing into unrelated, equally unprotected traversal code until the interpreter stack overflows.
53 lines
2.2 KiB
Python
53 lines
2.2 KiB
Python
import unittest
|
|
|
|
from benedict import benedict
|
|
|
|
|
|
class github_issue_0592_test_case(unittest.TestCase):
|
|
"""
|
|
This class describes a github issue 0592 test case.
|
|
https://github.com/fabiocaccamo/python-benedict/issues/592
|
|
|
|
To run this specific test:
|
|
- Run python -m unittest tests.github.test_issue_0592
|
|
"""
|
|
|
|
def test_assigning_dict_to_its_own_nested_descendant_does_not_raise(self) -> None:
|
|
# this specific case (assigning the dict into an empty nested
|
|
# descendant) is not self-referential yet at assignment time,
|
|
# so it must keep working exactly as before this fix.
|
|
d = benedict(keyattr_enabled=True, keyattr_dynamic=True)
|
|
d.a.b.c = d
|
|
self.assertIs(dict.__getitem__(d, "a")["b"]["c"], d)
|
|
|
|
def test_assigning_dict_to_its_own_nested_descendant_twice_does_not_recurse(
|
|
self,
|
|
) -> None:
|
|
# once `d` contains itself (from the first assignment), any further
|
|
# operation that needs to unwrap `d` again (eg. navigating into it
|
|
# to perform a second self-nesting assignment) must not raise
|
|
# RecursionError (it used to crash the Python/Jupyter process).
|
|
d = benedict(keyattr_enabled=True, keyattr_dynamic=True)
|
|
d.a.b.c = d
|
|
with self.assertRaises(ValueError):
|
|
d.a.b.d.e = d
|
|
|
|
def test_self_assignment_at_new_key_still_works(self) -> None:
|
|
# assigning a dict to itself under a brand new key must keep
|
|
# behaving exactly as before this fix (unaffected by it, verified
|
|
# against unpatched behavior: it does not raise, and the resulting
|
|
# structure is still a benedict, even though it stays cyclic).
|
|
d = benedict({"a": {"b": 1}})
|
|
d["self"] = d
|
|
self.assertIsInstance(d["self"], benedict)
|
|
self.assertEqual(d["self"]["a"], {"b": 1})
|
|
|
|
def test_normal_nested_dict_assignment_is_unaffected(self) -> None:
|
|
# sanity check: normal (non self-referential) nested benedict
|
|
# values must still be unwrapped/copied as before this fix.
|
|
inner = benedict({"x": 1})
|
|
outer = benedict()
|
|
outer["inner"] = inner
|
|
self.assertEqual(outer, {"inner": {"x": 1}})
|
|
self.assertIsNot(outer["inner"], inner)
|