From 9298678b216390a6d42e8ee3ff5e872e96f3e31e Mon Sep 17 00:00:00 2001 From: VenkateswarluNagineni Date: Wed, 15 Jul 2026 17:32:32 -0500 Subject: [PATCH] Fix dot-notation read on $@ref-backed ConfigParser proxies _ConfigProxy.__getattr__ fell back to getattr(self._value, key) when the chained config id was absent from the resolver. For a proxy backed by a $@ref the children have no ids of their own, so parser.alias.x looked up "x" as a dict *attribute* and raised AttributeError, even though parser.alias["x"] resolved it and returned the value. Resolve a key present in the underlying container the same way __getitem__ already does, so dot- and bracket-notation agree on ref-backed proxies and config keys keep precedence over dict methods, as documented. Keys absent from the container still fall back to the container's own attributes, so .keys()/.items() are unaffected. Signed-off-by: VenkateswarluNagineni --- monai/bundle/config_parser.py | 9 ++++++++- tests/bundle/test_config_parser.py | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/monai/bundle/config_parser.py b/monai/bundle/config_parser.py index 08c6fc296c..0e1ea4e77a 100644 --- a/monai/bundle/config_parser.py +++ b/monai/bundle/config_parser.py @@ -162,7 +162,14 @@ def __getattr__(self, key: str) -> Any: try: return self._chain(key) except KeyError: - return getattr(self._value, key) + pass + if isinstance(self._value, dict) and key in self._value: + # the chained id is absent from the resolver (for example when this proxy is + # backed by a `$@ref`, whose children have no ids of their own), but the key + # does exist in the container: resolve it like `__getitem__` does, so dot- and + # bracket-notation agree and config keys keep precedence over dict methods. + return self._value[key] + return getattr(self._value, key) def __getitem__(self, key: str | int) -> Any: try: diff --git a/tests/bundle/test_config_parser.py b/tests/bundle/test_config_parser.py index 546957ba7e..924287a299 100644 --- a/tests/bundle/test_config_parser.py +++ b/tests/bundle/test_config_parser.py @@ -487,6 +487,24 @@ def test_chained_ref_backed_proxy_write_through(self): del parser.alias["y"] self.assertNotIn("y", parser.get_parsed_content("target")) + def test_ref_backed_proxy_attribute_read(self): + # Dot-notation must agree with bracket-notation on a proxy reached via $@ref: + # "alias::x" has no id in the resolver, but "x" is a key of the aliased node, so + # both notations must resolve it (parser.alias.x raised AttributeError before this + # fix, while parser.alias["x"] returned the value). + parser = ConfigParser(config={"target": {"x": 1, "y": 2}, "alias": "$@target"}, globals={"monai": "monai"}) + self.assertEqual(parser.alias.x, parser.alias["x"]) + self.assertEqual(parser.alias.x, 1) + # a key absent from the container still falls back to the container's own methods + self.assertEqual(sorted(parser.alias.keys()), ["x", "y"]) + + def test_chained_ref_backed_proxy_attribute_read(self): + # dot-notation must follow the full ref chain, as _backing_id() does for writes. + parser = ConfigParser( + config={"target": {"x": 1}, "mid": "$@target", "alias": "$@mid"}, globals={"monai": "monai"} + ) + self.assertEqual(parser.alias.x, 1) + def test_raw_is_read_only(self): with self.assertRaises(AttributeError): self.parser.A._raw = {"something": "else"}