diff --git a/haystack/core/component/sockets.py b/haystack/core/component/sockets.py index 6a7d7c5db03..8120b15aa2e 100644 --- a/haystack/core/component/sockets.py +++ b/haystack/core/component/sockets.py @@ -125,7 +125,7 @@ def _component_name(self) -> str: def __getattribute__(self, name: Any) -> Any: try: - sockets = object.__getattribute__(self, "_sockets") + sockets = object.__getattribute__(self, "_sockets_dict") if name in sockets: return sockets[name] except AttributeError: diff --git a/releasenotes/notes/fix-sockets-getattribute-lookup-7c31a9f4e28b5d16.yaml b/releasenotes/notes/fix-sockets-getattribute-lookup-7c31a9f4e28b5d16.yaml new file mode 100644 index 00000000000..fec7bf87f08 --- /dev/null +++ b/releasenotes/notes/fix-sockets-getattribute-lookup-7c31a9f4e28b5d16.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + Fixed `Sockets.__getattribute__` looking up a `_sockets` attribute that no + instance has. The lookup raised `AttributeError` on every attribute access + and fell through to the default, so the intended fast path never ran and + socket access only worked through the copy `__init__` places in `__dict__`. + It now reads `_sockets_dict`, which removes an exception from every socket + attribute access. diff --git a/test/core/component/test_sockets.py b/test/core/component/test_sockets.py index e4fafe91ba9..c90cace4d76 100644 --- a/test/core/component/test_sockets.py +++ b/test/core/component/test_sockets.py @@ -51,6 +51,38 @@ def test_getattribute_non_existing_socket(self): with pytest.raises(AttributeError): io.input_3 + def test_getattribute_resolves_from_sockets_dict(self): + """`__getattribute__` must resolve sockets itself, not rely on the `__dict__` copy. + + It looked up `_sockets`, which no instance has, so the lookup always raised + AttributeError and fell through. Attribute access only worked because + `__init__` copies the sockets into `__dict__`. Removing that copy proves + which path is doing the work. + """ + comp = component_class("SomeComponent", input_types={"input_1": int})() + io = Sockets( + component=comp, + sockets_dict=comp.__haystack_input__._sockets_dict, # type: ignore[attr-defined] + sockets_io_type=InputSocket, + ) + del io.__dict__["input_1"] + + assert io.input_1 == comp.__haystack_input__._sockets_dict["input_1"] # type: ignore[attr-defined] + + def test_getattribute_does_not_shadow_methods_or_private_attributes(self): + """Resolving from `_sockets_dict` must not capture names the class itself defines.""" + comp = component_class("SomeComponent", input_types={"input_1": int})() + io = Sockets( + component=comp, + sockets_dict=comp.__haystack_input__._sockets_dict, # type: ignore[attr-defined] + sockets_io_type=InputSocket, + ) + + assert callable(io.get) + assert io._sockets_io_type is InputSocket + assert io._component is comp + assert isinstance(io._sockets_dict, dict) + def test_repr(self): comp = component_class("SomeComponent", input_types={"input_1": int, "input_2": int})() io = Sockets(