From e4c216c14bb751e098228822370b826326e0c4ec Mon Sep 17 00:00:00 2001 From: zhouhy Date: Wed, 14 Jan 2026 14:56:05 +0800 Subject: [PATCH 1/4] fix: deepcopy for nested dict --- configurize/data_class.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/configurize/data_class.py b/configurize/data_class.py index cddc407..fc6caf5 100644 --- a/configurize/data_class.py +++ b/configurize/data_class.py @@ -44,7 +44,7 @@ def _defined_attributes(self) -> set[str]: ) def _merge_args(self, kwargs: dict): - from copy import copy + from copy import deepcopy from typing import Callable for k, v in self.__class__._get_class_attributes().items(): @@ -53,7 +53,7 @@ def _merge_args(self, kwargs: dict): elif not k.startswith("_") and not isinstance( v, (Callable, cached_property, property, classmethod) ): - setattr(self, k, copy(v)) # Config build: copy class attr to object + setattr(self, k, deepcopy(v)) # Config build: copy class attr to object for k, v in kwargs.items(): setattr(self, k, v) From ace004fc7270a162e8b83b057ea0e550552c5c52 Mon Sep 17 00:00:00 2001 From: zhouhy Date: Wed, 14 Jan 2026 14:56:34 +0800 Subject: [PATCH 2/4] add hints & improve display --- configurize/config.py | 34 ++++++++++++++++++++++++++-------- configurize/reference.py | 29 ++++++++++++++++------------- configurize/utils.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 21 deletions(-) diff --git a/configurize/config.py b/configurize/config.py index 4f8424f..a6a3d47 100644 --- a/configurize/config.py +++ b/configurize/config.py @@ -12,7 +12,7 @@ from .allowed_types import recur_to_allowed_types from .data_class import DataClass from .reference import CfgReferenceError, Ref -from .utils import get_func_brief, writable_property +from .utils import filter_traceback_frames, get_func_brief, writable_property class TaskSpec(TypedDict, total=False): @@ -180,10 +180,16 @@ def _deref(self, name, value, deref=True): cur = self for action in value.actions: if action == ".": # goto father + if cur.father() is None: + raise CfgReferenceError( + f"Unable to find father of {cur._class_name} requested by <{self._class_name}.{name} = Ref('{value.ref_str}')>" + ) cur = cur.father() - assert cur is not None else: # goto sub - cur = super(Config, cur).__getattribute__(action) + if type(cur) is type: + cur = super(Config, cur).__getattribute__(cur, action) + else: + cur = super(Config, cur).__getattribute__(action) if isinstance(cur, Ref): ref_name = f"{self._get_node_name()}.{name}" raise ValueError( @@ -195,15 +201,27 @@ def _deref(self, name, value, deref=True): value = cur else: value.cur_value = cur - except (AttributeError, AssertionError, TypeError): + except (AttributeError, CfgReferenceError) as e: if value.default is CfgReferenceError: if deref: raise value.default( - f"Unable to find reference of <{value.ref_str}> @ {self._class_name}" - ) + f"Unable to find reference of <{self._class_name}.{name} = Ref('{value.ref_str}')>" + + ( + "\n\nReference Error took place when you try to access a Ref() but we can't find " + "the target in config tree. There might be 2 potential reason: \n" + "1. Typo in your ref string or wrong path.\n" + "2. The Ref() is accessed when the tree is not fully built. NOTE that " + "you should not access a Ref() during building (e.g. in __init__())!" + ), + e.with_traceback( + filter_traceback_frames("configurize", e.__traceback__) + ), + ) from None else: return value - return value.default + else: + return value.default + return value def _get_node_name(self) -> str: @@ -362,7 +380,7 @@ def __repr__(self): def _brief(self) -> str: self_repr = f"{self.__class__.__module__}.{self.__class__.__name__}" text = [f"{self_repr}("] - for k, v in self.items(): + for k, v in self.items(deref=False, rep=True): if isinstance(v, Config): sub_texts = v._brief().splitlines() sub_texts[0] = f"{k} = {sub_texts[0]}" diff --git a/configurize/reference.py b/configurize/reference.py index bce7e4a..43b25df 100644 --- a/configurize/reference.py +++ b/configurize/reference.py @@ -1,4 +1,9 @@ -class CfgReferenceError(LookupError): +class CfgReferenceError(Exception): + def __init__(self, err_str, raw_exec=None): + super().__init__() + self.err_str = err_str + self.raw_exec = raw_exec + def __str__(self): import traceback @@ -10,17 +15,11 @@ def __str__(self): if frame.f_code.co_name == "__init__": _self = frame.f_locals["self"] error_hint = f"\n\nHint: Find you access this in {_self._class_name}.__init__() during build!" - return ( - super().__str__() - + ( - "\n\nReference Error took place when you try to access a Ref() but we can't find " - "the target in config tree. There might be 2 potential reason: \n" - "1. Typo in your ref string or wrong path.\n" - "2. The Ref() is accessed when the tree is not fully built. NOTE that " - "you should not access a Ref() during bulding (e.g. in __init__())!" - ) - + error_hint - ) + if self.raw_exec: + raw_tb = traceback.format_exception(self.raw_exec) + raw_tb = "\n".join(raw_tb) + error_hint += f"\n\nException during de-ref:\n\n{raw_tb}" + return self.err_str + error_hint class Ref: @@ -70,7 +69,11 @@ def _parse_level(ref_str): return levels def __repr__(self) -> str: - if self.cur_value is KeyError: + if self.cur_value is CfgReferenceError: return f"❓ PendingRef({self.ref_str})" else: + from .config import Config + + if isinstance(self.cur_value, Config): + return f"{self.cur_value._get_node_name()} 🈯 Ref({self.ref_str})" return f"{repr(self.cur_value)} 🈯 Ref({self.ref_str})" diff --git a/configurize/utils.py b/configurize/utils.py index 7d5a8d7..4607b3b 100644 --- a/configurize/utils.py +++ b/configurize/utils.py @@ -2,6 +2,8 @@ import inspect import os +import sys +import types from typing import TYPE_CHECKING, Any, Callable if TYPE_CHECKING: @@ -20,6 +22,35 @@ def get_func_brief(func): return f"Func{signature}" +def filter_traceback_frames( + module_pattern, tb: types.TracebackType = None +) -> types.TracebackType: + """过滤特定模块的traceback帧""" + if tb is None: + _, _, tb = sys.exc_info() + filtered: list[types.FrameType] = [] + while tb is not None: + # 检查模块名是否匹配排除条件 + frame = tb.tb_frame + module_name = frame.f_globals.get("__name__", "") + if not module_name.startswith(module_pattern): + # 保留非目标模块的帧 + filtered.append(frame) + tb = tb.tb_next + + new_tb = None + + # 反向构建新traceback链(FILO) + for frame in reversed(filtered): + new_tb = types.TracebackType( + new_tb, + tb_frame=frame, + tb_lasti=frame.f_lasti, + tb_lineno=frame.f_lineno, + ) + return new_tb + + def get_object_from_file(file: str, name: str = "Exp") -> object: """ get object by file. From 094c5c3467d2acd70a712dbbd6b9a2a70cf33fe2 Mon Sep 17 00:00:00 2001 From: zhouhy Date: Wed, 14 Jan 2026 14:57:30 +0800 Subject: [PATCH 3/4] update version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4362243..f31ab82 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "configurize" -version = "0.1.4" +version = "0.1.5" description = "Hierarchical configuration management with inheritance, cross-references, and diffing" readme = "README.md" license = {text = "MIT"} From f1072a77e4a59c6bb35825980aafa5af6b495fbd Mon Sep 17 00:00:00 2001 From: zhouhy Date: Wed, 14 Jan 2026 15:18:10 +0800 Subject: [PATCH 4/4] do not de-ref at sanity_check & trace --- configurize/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/configurize/config.py b/configurize/config.py index a6a3d47..e9876b1 100644 --- a/configurize/config.py +++ b/configurize/config.py @@ -275,7 +275,7 @@ def _all_set_history(self) -> dict[str, list[tuple[Any, str, bool]]]: if self._set_attribute_traces: for k, trace in self._set_attribute_traces.items(): history[f"{self._get_node_name()}.{k}"] = trace - for k, v in self.items(): + for k, v in self.items(deref=False): if isinstance(v, Config): history.update(v._all_set_history()) return history @@ -305,7 +305,7 @@ def __copy__(self): def sanity_check(self): # recursive check sub-configs - for k, v in self.items(): + for k, v in self.items(deref=False): if isinstance(v, Config): v.sanity_check()