Skip to content
Open
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
2 changes: 1 addition & 1 deletion haystack/core/component/sockets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
32 changes: 32 additions & 0 deletions test/core/component/test_sockets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down