From a020dd1375c66d4c85052ac6551f653c05c62560 Mon Sep 17 00:00:00 2001 From: Mohammed Razak <mohammedrazak2001@gmail.com> Date: Thu, 13 Mar 2025 01:19:44 +0530 Subject: [PATCH 1/4] Add compatibility checks for Callable types --- haystack/core/type_utils.py | 30 ++++++++++++-- test/core/pipeline/test_type_utils.py | 59 ++++++++++++++++++++++++++- 2 files changed, 84 insertions(+), 5 deletions(-) diff --git a/haystack/core/type_utils.py b/haystack/core/type_utils.py index 162fcc759f..ba093283ca 100644 --- a/haystack/core/type_utils.py +++ b/haystack/core/type_utils.py @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai> # # SPDX-License-Identifier: Apache-2.0 - +import collections.abc from typing import Any, TypeVar, Union, get_args, get_origin from haystack import logging @@ -65,15 +65,37 @@ def _strict_types_are_compatible(sender, receiver): # pylint: disable=too-many- sender_args = get_args(sender) receiver_args = get_args(receiver) + # Handle Callable types + if sender_origin == receiver_origin == collections.abc.Callable: + return _check_callable_compatibility(sender_args, receiver_args) + # Handle bare types if not sender_args and sender_origin: sender_args = (Any,) if not receiver_args and receiver_origin: receiver_args = (Any,) * (len(sender_args) if sender_args else 1) - if len(sender_args) > len(receiver_args): - return False - return all(_strict_types_are_compatible(*args) for args in zip(sender_args, receiver_args)) + return not (len(sender_args) > len(receiver_args)) and all( + _strict_types_are_compatible(*args) for args in zip(sender_args, receiver_args) + ) + + +def _check_callable_compatibility(sender_args, receiver_args): + """Helper function to check compatibility of Callable types""" + if not receiver_args: + return True + if not sender_args: + sender_args = ([Any] * len(receiver_args[0]), Any) + # Standard Callable has two elements in args: argument list and return type + if len(sender_args) != 2 or len(receiver_args) != 2: + return False + # Return types must be compatible + if not _strict_types_are_compatible(sender_args[1], receiver_args[1]): + return False + # Input Arguments must be of same length + if len(sender_args[0]) != len(receiver_args[0]): + return False + return all(_strict_types_are_compatible(sender_args[0][i], receiver_args[0][i]) for i in range(len(sender_args[0]))) def _type_name(type_): diff --git a/test/core/pipeline/test_type_utils.py b/test/core/pipeline/test_type_utils.py index 9151b2770c..628d16d9c9 100644 --- a/test/core/pipeline/test_type_utils.py +++ b/test/core/pipeline/test_type_utils.py @@ -3,7 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 from enum import Enum from pathlib import Path -from typing import Any, Dict, Iterable, List, Literal, Mapping, Optional, Sequence, Set, Tuple, Union +from typing import Any, Callable, Dict, Iterable, List, Literal, Mapping, Optional, Sequence, Set, Tuple, Union import pytest @@ -384,6 +384,7 @@ def test_container_of_primitive_to_bare_container_strict(sender_type, receiver_t pytest.param(Dict[Any, Any], Dict, id="dict-of-any-to-bare-dict"), pytest.param(Set[Any], Set, id="set-of-any-to-bare-set"), pytest.param(Tuple[Any], Tuple, id="tuple-of-any-to-bare-tuple"), + pytest.param(Callable[[Any], Any], Callable, id="callable-of-any-to-bare-callable"), ], ) def test_container_of_any_to_bare_container_strict(sender_type, receiver_type): @@ -438,3 +439,59 @@ def test_nested_container_compatibility(sender_type, receiver_type): assert _types_are_compatible(sender_type, receiver_type) # Bare container types should not be compatible with their typed counterparts assert not _types_are_compatible(receiver_type, sender_type) + + +@pytest.mark.parametrize( + "sender_type,receiver_type", + [ + pytest.param(Callable[[int, str], bool], Callable, id="callable-to-bare-callable"), + pytest.param(Callable[[List], int], Callable[[List], Any], id="callable-list-int-to-any"), + ], +) +def test_callable_compatibility(sender_type, receiver_type): + assert _types_are_compatible(sender_type, receiver_type) + assert not _types_are_compatible(receiver_type, sender_type) + + +@pytest.mark.parametrize( + "sender_type,receiver_type", + [ + pytest.param( + Callable[[Callable[[int], str]], List[str]], Callable, id="callable-with-nested-types-to-bare-callable" + ), + pytest.param( + Callable[[Callable[[int], str]], List[str]], + Callable[[Callable[[Any], Any]], Any], + id="double-nested-callable", + ), + pytest.param( + Callable[[Callable[[Callable[[int], str]], bool]], str], + Callable[[Callable[[Callable[[Any], Any]], Any]], Any], + id="triple-nested-callable", + ), + ], +) +def test_nested_callable_compatibility(sender_type, receiver_type): + assert _types_are_compatible(sender_type, receiver_type) + # Bare callable container types should not be compatible with their typed counterparts + assert not _types_are_compatible(receiver_type, sender_type) + + +@pytest.mark.parametrize( + "sender_type,receiver_type", + [ + pytest.param( + Callable[[int, str], bool], Callable[[int, str], List], id="callable-to-callable-with-different-return-type" + ), + pytest.param( + Callable[[int, int], bool], Callable[[int, str], bool], id="callable-to-callable-with-different-args-type" + ), + pytest.param( + Callable[[int, str], bool], Callable[[int, str, int], bool], id="callable-to-callable-with-different-args" + ), + pytest.param(Callable[[int, str], bool], Callable[[int], bool], id="callable-to-callable-with-fewer-args"), + ], +) +def test_always_incompatible_callable_types(sender_type, receiver_type): + assert not _types_are_compatible(sender_type, receiver_type) + assert not _types_are_compatible(receiver_type, sender_type) From c5f8dfa97f2618f6580af94922ceeadc8ddd0db2 Mon Sep 17 00:00:00 2001 From: Mohammed Razak <mohammedrazak2001@gmail.com> Date: Thu, 13 Mar 2025 01:25:09 +0530 Subject: [PATCH 2/4] add release notes --- .../add-callable-type-compatibility-a3746cd0cfd8fc10.yaml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 releasenotes/notes/add-callable-type-compatibility-a3746cd0cfd8fc10.yaml diff --git a/releasenotes/notes/add-callable-type-compatibility-a3746cd0cfd8fc10.yaml b/releasenotes/notes/add-callable-type-compatibility-a3746cd0cfd8fc10.yaml new file mode 100644 index 0000000000..2ffe92cdb6 --- /dev/null +++ b/releasenotes/notes/add-callable-type-compatibility-a3746cd0cfd8fc10.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add compatibility for Callable types. From dacd5bb5da86103bc0793de48da53098663f8332 Mon Sep 17 00:00:00 2001 From: Michele Pangrazzi <xmikex83@gmail.com> Date: Wed, 26 Mar 2025 16:38:58 +0100 Subject: [PATCH 3/4] Update test_type_utils.py Fix license header --- test/core/pipeline/test_type_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/core/pipeline/test_type_utils.py b/test/core/pipeline/test_type_utils.py index 628d16d9c9..e6dcf1baa3 100644 --- a/test/core/pipeline/test_type_utils.py +++ b/test/core/pipeline/test_type_utils.py @@ -1,6 +1,7 @@ # SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai> # # SPDX-License-Identifier: Apache-2.0 + from enum import Enum from pathlib import Path from typing import Any, Callable, Dict, Iterable, List, Literal, Mapping, Optional, Sequence, Set, Tuple, Union From 59a88bd984fdf4a89639dcd6de0daff43ffcf847 Mon Sep 17 00:00:00 2001 From: Michele Pangrazzi <xmikex83@gmail.com> Date: Wed, 26 Mar 2025 16:45:20 +0100 Subject: [PATCH 4/4] Update type_utils.py Fix license header --- haystack/core/type_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/haystack/core/type_utils.py b/haystack/core/type_utils.py index 3991818173..40dc6e4d4c 100644 --- a/haystack/core/type_utils.py +++ b/haystack/core/type_utils.py @@ -1,6 +1,7 @@ # SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai> # # SPDX-License-Identifier: Apache-2.0 + import collections.abc from typing import Any, TypeVar, Union, get_args, get_origin