Repository: deepset-ai/haystack

Issue:
Enhance Component I/O representation
After #6768 is done we should have a nice way of representing a Component's I/O.

Example:
```py
from haystack.components.converters.txt import TextFileToDocument

converter = TextFileToDocument()

print(converter.inputs)
>>> Inputs:
>>>   * sources: List[Document]
>>>   * meta: Dict[str, Any], default: {}
```




Discussion:
Instead of adding an `inputs` field, we could leverage `__repr__` to get:

```py
print(converter)
>>> Inputs:
>>>   * sources: List[Document]
>>>   * meta: Dict[str, Any], default: {}
```

Resolving patch:
diff --git a/haystack/core/component/component.py b/haystack/core/component/component.py
--- a/haystack/core/component/component.py
+++ b/haystack/core/component/component.py
@@ -160,6 +160,21 @@ def __call__(cls, *args, **kwargs):
         return instance
 
 
+def _component_repr(component: Component) -> str:
+    """
+    All Components override their __repr__ method with this one.
+    It prints the component name and the input/output sockets.
+    """
+    result = object.__repr__(component)
+    if pipeline := getattr(component, "__haystack_added_to_pipeline__"):
+        # This Component has been added in a Pipeline, let's get the name from there.
+        result += f"\n{pipeline.get_component_name(component)}"
+
+    # We're explicitly ignoring the type here because we're sure that the component
+    # has the __haystack_input__ and __haystack_output__ attributes at this point
+    return f"{result}\n{component.__haystack_input__}\n{component.__haystack_output__}"  # type: ignore[attr-defined]
+
+
 class _Component:
     """
     See module's docstring.
@@ -332,6 +347,9 @@ def copy_class_namespace(namespace):
         self.registry[class_path] = class_
         logger.debug("Registered Component %s", class_)
 
+        # Override the __repr__ method with a default one
+        class_.__repr__ = _component_repr
+
         return class_
 
     def __call__(self, class_):
diff --git a/haystack/core/component/sockets.py b/haystack/core/component/sockets.py
--- a/haystack/core/component/sockets.py
+++ b/haystack/core/component/sockets.py
@@ -82,8 +82,9 @@ def _component_name(self) -> str:
             return pipeline.get_component_name(self._component)
 
         # This Component has not been added to a Pipeline yet, so we can't know its name.
-        # Let's use the class name instead.
-        return str(self._component)
+        # Let's use default __repr__. We don't call repr() directly as Components have a custom
+        # __repr__ method and that would lead to infinite recursion since we call Sockets.__repr__ in it.
+        return object.__repr__(self._component)
 
     def __getattribute__(self, name):
         try:
@@ -96,12 +97,10 @@ def __getattribute__(self, name):
         return object.__getattribute__(self, name)
 
     def __repr__(self) -> str:
-        result = self._component_name()
+        result = ""
         if self._sockets_io_type == InputSocket:
-            result += " inputs:\n"
+            result = "Inputs:\n"
         elif self._sockets_io_type == OutputSocket:
-            result += " outputs:\n"
+            result = "Outputs:\n"
 
-        result += "\n".join([f"  - {n}: {_type_name(s.type)}" for n, s in self._sockets_dict.items()])
-
-        return result
+        return result + "\n".join([f"  - {n}: {_type_name(s.type)}" for n, s in self._sockets_dict.items()])


Test patch:
diff --git a/test/core/component/test_component.py b/test/core/component/test_component.py
--- a/test/core/component/test_component.py
+++ b/test/core/component/test_component.py
@@ -4,6 +4,7 @@
 
 from haystack.core.component import Component, InputSocket, OutputSocket, component
 from haystack.core.errors import ComponentError
+from haystack.core.pipeline import Pipeline
 
 
 def test_correct_declaration():
@@ -189,3 +190,31 @@ def run(self, *, arg: int):
     comp = MockComponent()
     component_inputs = {name: {"type": socket.type} for name, socket in comp.__haystack_input__._sockets_dict.items()}
     assert component_inputs == {"arg": {"type": int}}
+
+
+def test_repr():
+    @component
+    class MockComponent:
+        def __init__(self):
+            component.set_output_types(self, value=int)
+
+        def run(self, value: int):
+            return {"value": value}
+
+    comp = MockComponent()
+    assert repr(comp) == f"{object.__repr__(comp)}\nInputs:\n  - value: int\nOutputs:\n  - value: int"
+
+
+def test_repr_added_to_pipeline():
+    @component
+    class MockComponent:
+        def __init__(self):
+            component.set_output_types(self, value=int)
+
+        def run(self, value: int):
+            return {"value": value}
+
+    pipe = Pipeline()
+    comp = MockComponent()
+    pipe.add_component("my_component", comp)
+    assert repr(comp) == f"{object.__repr__(comp)}\nmy_component\nInputs:\n  - value: int\nOutputs:\n  - value: int"
diff --git a/test/core/component/test_sockets.py b/test/core/component/test_sockets.py
--- a/test/core/component/test_sockets.py
+++ b/test/core/component/test_sockets.py
@@ -23,19 +23,6 @@ def test_init_with_empty_sockets(self):
         assert io._component == comp
         assert io._sockets_dict == {}
 
-    def test_component_name(self):
-        comp = component_class("SomeComponent")()
-        io = Sockets(component=comp, sockets_dict={}, sockets_io_type=InputSocket)
-        assert io._component_name() == str(comp)
-
-    def test_component_name_added_to_pipeline(self):
-        comp = component_class("SomeComponent")()
-        pipeline = Pipeline()
-        pipeline.add_component("my_component", comp)
-
-        io = Sockets(component=comp, sockets_dict={}, sockets_io_type=InputSocket)
-        assert io._component_name() == "my_component"
-
     def test_getattribute(self):
         comp = component_class("SomeComponent", input_types={"input_1": int, "input_2": int})()
         io = Sockets(component=comp, sockets_dict=comp.__haystack_input__._sockets_dict, sockets_io_type=InputSocket)
@@ -54,4 +41,4 @@ def test_repr(self):
         comp = component_class("SomeComponent", input_types={"input_1": int, "input_2": int})()
         io = Sockets(component=comp, sockets_dict=comp.__haystack_input__._sockets_dict, sockets_io_type=InputSocket)
         res = repr(io)
-        assert res == f"{comp} inputs:\n  - input_1: int\n  - input_2: int"
+        assert res == "Inputs:\n  - input_1: int\n  - input_2: int"