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"}