Skip to content
Open
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
27 changes: 22 additions & 5 deletions benedict/dicts/base/base_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,32 @@ class BaseDict(dict[_K, _V]):
_dict: dict[_K, _V] | None
_frozen: bool

# ids of mappings being unwrapped anywhere on the call stack, to detect
# self-referential structures instead of recursing forever
_unwrapping_ids: set[int] = set()

@classmethod
def _get_dict_or_value(cls, value: Any) -> Any:
value = value.dict() if isinstance(value, cls) else value
if isinstance(value, MutableMapping):
for key in value.keys():
key_val = value[key]
if isinstance(key_val, cls):
key_val = cls._get_dict_or_value(value[key])
value[key] = key_val
value_id = id(value)
if value_id in cls._unwrapping_ids:
# it contains itself; unwind here before reaching code with no cycle
# protection of its own
raise ValueError(
"Cannot assign a dict that contains itself "
"(directly or indirectly): self-referential "
"(cyclic) structures are not supported."
)
cls._unwrapping_ids.add(value_id)
try:
for key in value.keys():
key_val = value[key]
if isinstance(key_val, cls):
key_val = cls._get_dict_or_value(value[key])
value[key] = key_val
finally:
cls._unwrapping_ids.discard(value_id)
return value

def __new__(cls, *args: Any, **kwargs: Any) -> Self:
Expand Down
44 changes: 44 additions & 0 deletions tests/github/test_issue_0592.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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:
# not self-referential yet at assignment time, so it must keep working
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, unwrapping it again must not raise
# RecursionError (it used to crash the 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 new key is unaffected by the fix
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:
# non self-referential values must still be unwrapped as before
inner = benedict({"x": 1})
outer = benedict()
outer["inner"] = inner
self.assertEqual(outer, {"inner": {"x": 1}})
self.assertIsNot(outer["inner"], inner)