diff --git a/haystack/core/pipeline/utils.py b/haystack/core/pipeline/utils.py index 3c112df8c49..d1263dec973 100644 --- a/haystack/core/pipeline/utils.py +++ b/haystack/core/pipeline/utils.py @@ -33,6 +33,11 @@ def _deepcopy_with_exceptions(obj: Any) -> Any: from haystack.tools.tool import Tool from haystack.tools.toolset import Toolset + # namedtuples are tuple subclasses whose __new__ takes the fields positionally, + # so they must be rebuilt by unpacking rather than from a single iterable. + if isinstance(obj, tuple) and hasattr(obj, "_fields"): + return type(obj)(*(_deepcopy_with_exceptions(v) for v in obj)) + if isinstance(obj, (list, tuple, set)): return type(obj)(_deepcopy_with_exceptions(v) for v in obj) diff --git a/releasenotes/notes/fix-deepcopy-namedtuple-c7aa157bc9aef85b.yaml b/releasenotes/notes/fix-deepcopy-namedtuple-c7aa157bc9aef85b.yaml new file mode 100644 index 00000000000..7cc1b4919e0 --- /dev/null +++ b/releasenotes/notes/fix-deepcopy-namedtuple-c7aa157bc9aef85b.yaml @@ -0,0 +1,10 @@ +--- +fixes: + - | + Fixed ``_deepcopy_with_exceptions`` crashing when a value is a ``namedtuple`` + (or any ``typing.NamedTuple``). The list/tuple/set branch rebuilt the + container with ``type(obj)()``, but a namedtuple's ``__new__`` + expects its fields as positional arguments rather than a single iterable, so + copying a component input or parameter that contained a namedtuple raised a + ``TypeError``. Namedtuples are now rebuilt by unpacking their deep-copied + fields. diff --git a/test/core/pipeline/test_utils.py b/test/core/pipeline/test_utils.py index ad4128e1b19..c9e1d9831ff 100644 --- a/test/core/pipeline/test_utils.py +++ b/test/core/pipeline/test_utils.py @@ -4,6 +4,7 @@ import logging import warnings +from collections import namedtuple import pytest @@ -255,6 +256,19 @@ def test_deepcopy_with_fallback_component(self, monkeypatch): res = _deepcopy_with_exceptions(original) assert res["component"] is original["component"] + def test_deepcopy_with_fallback_namedtuple(self): + Point = namedtuple("Point", ["x", "y"]) + inner = Copyable() + original = {"point": Point(inner, 2)} + copy = _deepcopy_with_exceptions(original) + # A namedtuple must be reconstructed as its own type. Its __new__ takes + # positional fields, so the plain ``type(obj)()`` path used for + # lists/tuples/sets would raise a TypeError instead of copying it. + assert isinstance(copy["point"], Point) + assert copy["point"].y == 2 + # Its contents are deep-copied, matching how plain tuples are handled. + assert copy["point"].x is not original["point"].x + class TestArgsDeprecated: @pytest.fixture