Description
ConfigManager._remove_circular_references in pdf2zh/config.py:62 treats any object seen twice as a cycle. It threads a single shared seen set through the whole traversal (pdf2zh/config.py:64-69) and never removes an id when recursion unwinds, so a diamond (shared but non-circular) reference is wrongly replaced with None at pdf2zh/config.py:67-68. Since _save_config (pdf2zh/config.py:54-60) persists the cleaned data, saving a config containing the same dict/list object in two places silently drops the second occurrence (data loss), even though the data is perfectly JSON-serializable.
Reproduce
- From the repository root, run:
python -c 'from pdf2zh.config import ConfigManager; m=ConfigManager.__new__(ConfigManager); shared={"a":1}; obj={"x":shared,"y":shared}; print(m._remove_circular_references(obj))'
Observed result: {'x': {'a': 1}, 'y': None} — the second reference is nulled though there is no cycle.
- List case:
python -c 'from pdf2zh.config import ConfigManager; m=ConfigManager.__new__(ConfigManager); s=[1,2]; print(m._remove_circular_references([s,s]))'
Observed result: [[1, 2], None] — same wrongful nulling.
- Sanity check that true cycles are the intended target: a self-referential dict still collapses to
None as expected, so the method conflates diamonds with cycles.
Expected
Only true ancestor-path cycles should collapse to None; shared references reachable via sibling branches must be preserved verbatim. Smallest fix: scope seen to the current recursion path in _remove_circular_references — add the object id before recursing into its children and discard it afterwards (or pass a copied/extended set down), instead of one never-shrunk global set.
Checklist
Searched open and closed issues and pull requests (circular, ConfigManager, save_config shared reference None, ZhipuTranslator/do_translate variants, AnythingLLM lang_in lang_out swapped, tencent chunk split MAX_CHARS) -- zero overlap. File:line + repro provided.
Distinct from #766 (#766, "Why circular dependency?"): that issue is about running modules in PyCharm / import setup, not about ConfigManager._remove_circular_references nulling shared config values on save.
Description
ConfigManager._remove_circular_referencesinpdf2zh/config.py:62treats any object seen twice as a cycle. It threads a single sharedseenset through the whole traversal (pdf2zh/config.py:64-69) and never removes an id when recursion unwinds, so a diamond (shared but non-circular) reference is wrongly replaced withNoneatpdf2zh/config.py:67-68. Since_save_config(pdf2zh/config.py:54-60) persists the cleaned data, saving a config containing the same dict/list object in two places silently drops the second occurrence (data loss), even though the data is perfectly JSON-serializable.Reproduce
python -c 'from pdf2zh.config import ConfigManager; m=ConfigManager.__new__(ConfigManager); shared={"a":1}; obj={"x":shared,"y":shared}; print(m._remove_circular_references(obj))'Observed result:
{'x': {'a': 1}, 'y': None}— the second reference is nulled though there is no cycle.python -c 'from pdf2zh.config import ConfigManager; m=ConfigManager.__new__(ConfigManager); s=[1,2]; print(m._remove_circular_references([s,s]))'Observed result:
[[1, 2], None]— same wrongful nulling.Noneas expected, so the method conflates diamonds with cycles.Expected
Only true ancestor-path cycles should collapse to
None; shared references reachable via sibling branches must be preserved verbatim. Smallest fix: scopeseento the current recursion path in_remove_circular_references— add the object id before recursing into its children and discard it afterwards (or pass a copied/extended set down), instead of one never-shrunk global set.Checklist
Searched open and closed issues and pull requests (
circular,ConfigManager,save_config shared reference None,ZhipuTranslator/do_translatevariants,AnythingLLM lang_in lang_out swapped,tencent chunk split MAX_CHARS) -- zero overlap. File:line + repro provided.Distinct from #766 (#766, "Why circular dependency?"): that issue is about running modules in PyCharm / import setup, not about
ConfigManager._remove_circular_referencesnulling shared config values on save.