Skip to content
Merged
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
38 changes: 28 additions & 10 deletions configurize/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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(
Expand All @@ -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:
Expand Down Expand Up @@ -257,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
Expand Down Expand Up @@ -287,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()

Expand Down Expand Up @@ -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]}"
Expand Down
4 changes: 2 additions & 2 deletions configurize/data_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -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)

Expand Down
29 changes: 16 additions & 13 deletions configurize/reference.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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:
Expand Down Expand Up @@ -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})"
31 changes: 31 additions & 0 deletions configurize/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import inspect
import os
import sys
import types
from typing import TYPE_CHECKING, Any, Callable

if TYPE_CHECKING:
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"}
Expand Down