Fix RecursionError when assigning a benedict into its own nested descendant (#592) - #597
Open
dualfroz wants to merge 1 commit into
Open
Fix RecursionError when assigning a benedict into its own nested descendant (#592)#597dualfroz wants to merge 1 commit into
dualfroz wants to merge 1 commit into
Conversation
…#592) 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.
dualfroz
force-pushed
the
dualfroz/fix-recursion-self-nesting
branch
from
September 5, 2026 23:10
beb3578 to
545cbf4
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #597 +/- ##
=======================================
Coverage 98.38% 98.39%
=======================================
Files 64 64
Lines 2418 2425 +7
=======================================
+ Hits 2379 2386 +7
Misses 39 39
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
#592
Assigning a
benedictinto a nested descendant of itself works once, but asecond such assignment crashes with
RecursionError(and, per the issue,can crash a Jupyter kernel):
Root cause
BaseDict._get_dict_or_value()inbenedict/dicts/base/base_dict.py(lines25-34 on
main) recursively unwraps nestedbenedictvalues with no cycledetection, and it is called from every
__init__/__setitem__/update/setdefault. After the first assignment,testcontains a value that(transitively) is
testitself. Any later operation that needs to unwraptestagain (eg. navigatingtest.ato perform the second assignment)recurses into that self-reference and never terminates.
The actual recursion is not a simple self-call of
_get_dict_or_value: italternates between
_get_dict_or_value,_cast/__init__(which wrap thesame underlying raw dict into a fresh
benedictobject on every access) and__setitem__->check_keys/traverse(benedict/core/traverse.py, whichhas no cycle protection of its own either). All of these frames belong to
one continuous call stack, so a cycle guard confined to
_get_dict_or_valuestill sees the re-entry and can stop the whole chainbefore the uncontrolled recursion is ever reached elsewhere.
Fix
benedict/dicts/base/base_dict.py:_get_dict_or_valuenow tracks, in aclass-level
_unwrapping_idsset, theid()of every mapping it iscurrently in the middle of unwrapping (added on entry, removed via
finallyon exit -- so it only ever reflects containers that are live onthe current call stack, not a permanent "already seen" memo, which would
incorrectly flag legitimate shared/aliased references as cycles). If a
mapping is encountered while it is already being unwrapped higher up the
stack, the structure is self-referential: instead of recursing again (which
would eventually blow the stack, potentially inside unrelated,
unprotected code such as
check_keys/traverse), aValueErroris raisedimmediately, unwinding the whole unwrap/cast chain in a controlled way.
Normal (non-cyclic) nested-dict unwrapping is unaffected: the guard is only
ever populated with ids of mappings still being processed, and a proper
(acyclic) tree can never revisit a node that is still being visited, so the
new code paths are simply never exercised for legitimate data.
Scope note: a complete "silently supported" self-reference (making the
second assignment succeed rather than raise) is not achievable without
also touching
benedict/core/traverse.py's_traverse_dict/_traverse_collection(used, viacheck_keys, on every__setitem__, andalso by several other public APIs such as
merge/flatten/keypaths),since that traversal is independently unprotected against cycles. That was
considered out of scope for a minimal, low-risk fix, so this change turns
the uncontrolled
RecursionErrorinto a clear, immediateValueErrorinstead.