Coverage for haystack/core/super_component/utils.py: 96%
95 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 13:53 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 13:53 +0000
1# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
2#
3# SPDX-License-Identifier: Apache-2.0
5from types import UnionType
6from typing import Annotated, Any, get_args, get_origin
8from haystack.core.component.types import HAYSTACK_GREEDY_VARIADIC_ANNOTATION, HAYSTACK_VARIADIC_ANNOTATION
9from haystack.utils.type_serialization import _build_pep604_union_type, _is_union_type
12class _delegate_default:
13 """Custom object for delegating filling of default values to the underlying components."""
16def _is_compatible(
17 type1: type | UnionType, type2: type | UnionType, unwrap_nested: bool = True
18) -> tuple[bool, type | UnionType | None]:
19 """
20 Check if two types are compatible (bidirectional/symmetric check).
22 :param type1: First type to compare
23 :param type2: Second type to compare
24 :param unwrap_nested: If True, recursively unwraps nested Optional and Variadic types.
25 If False, only unwraps at the top level.
26 :return: Tuple of (True if types are compatible, common type if compatible)
27 """
28 type1_unwrapped = _unwrap_all(type1, recursive=unwrap_nested)
29 type2_unwrapped = _unwrap_all(type2, recursive=unwrap_nested)
31 return _types_are_compatible(type1_unwrapped, type2_unwrapped)
34def _types_are_compatible(type1: type | UnionType, type2: type | UnionType) -> tuple[bool, type | UnionType | None]:
35 """
36 Core type compatibility check implementing symmetric matching.
38 :param type1: First unwrapped type to compare
39 :param type2: Second unwrapped type to compare
40 :return: True if types are compatible, False otherwise
41 """
42 # Handle Any type
43 if type1 is Any:
44 return True, type2
45 if type2 is Any:
46 return True, type1
48 # Direct equality
49 if type1 == type2:
50 return True, type1
52 type1_origin = get_origin(type1)
53 type2_origin = get_origin(type2)
55 # Handle Union types (including X | Y syntax)
56 if _is_union_type(type1_origin) or _is_union_type(type2_origin):
57 return _check_union_compatibility(type1, type2, type1_origin, type2_origin)
59 # Handle non-Union types
60 return _check_non_union_compatibility(type1, type2, type1_origin, type2_origin)
63def _check_union_compatibility(
64 type1: type | UnionType, type2: type | UnionType, type1_origin: Any, type2_origin: Any
65) -> tuple[bool, type | UnionType | None]:
66 """Handle all Union type compatibility cases (including X | Y syntax)."""
67 if _is_union_type(type1_origin) and not _is_union_type(type2_origin):
68 # Find all compatible types from the union
69 compatible_types = []
70 for union_arg in get_args(type1):
71 is_compat, common = _types_are_compatible(union_arg, type2)
72 if is_compat and common is not None:
73 compatible_types.append(common)
74 if compatible_types:
75 result_type = _build_pep604_union_type(compatible_types)
76 return True, result_type
77 return False, None
79 if _is_union_type(type2_origin) and not _is_union_type(type1_origin):
80 # Find all compatible types from the union
81 compatible_types = []
82 for union_arg in get_args(type2):
83 is_compat, common = _types_are_compatible(type1, union_arg)
84 if is_compat and common is not None:
85 compatible_types.append(common)
86 if compatible_types:
87 result_type = _build_pep604_union_type(compatible_types)
88 return True, result_type
89 return False, None
91 # Both are Union types
92 compatible_types = []
93 for arg1 in get_args(type1):
94 for arg2 in get_args(type2):
95 is_compat, common = _types_are_compatible(arg1, arg2)
96 if is_compat and common is not None:
97 compatible_types.append(common)
99 if compatible_types:
100 result_type = _build_pep604_union_type(compatible_types)
101 return True, result_type
102 return False, None
105def _check_non_union_compatibility(
106 type1: type | UnionType, type2: type | UnionType, type1_origin: Any, type2_origin: Any
107) -> tuple[bool, type | UnionType | None]:
108 """Handle non-Union type compatibility cases."""
109 # If no origin, compare types directly
110 if not type1_origin and not type2_origin:
111 if type1 == type2:
112 return True, type1
113 return False, None
115 # Both must have origins and they must be equal
116 if not (type1_origin and type2_origin and type1_origin == type2_origin):
117 return False, None
119 # Compare generic type arguments
120 type1_args = get_args(type1)
121 type2_args = get_args(type2)
123 if len(type1_args) != len(type2_args):
124 return False, None
126 # Check if all arguments are compatible
127 common_args = []
128 for t1_arg, t2_arg in zip(type1_args, type2_args, strict=True):
129 is_compat, common = _types_are_compatible(t1_arg, t2_arg)
130 if not is_compat:
131 return False, None
132 common_args.append(common)
134 # Reconstruct the type with common arguments
135 return True, type1_origin[tuple(common_args)]
138def _unwrap_all(t: type | UnionType, recursive: bool) -> type | UnionType:
139 """
140 Unwrap a type until no more unwrapping is possible.
142 :param t: Type to unwrap
143 :param recursive: If True, recursively unwraps nested types
144 :return: The fully unwrapped type
145 """
146 # First handle top-level Variadic/GreedyVariadic
147 if _is_variadic_type(t):
148 t = _unwrap_variadics(t, recursive=recursive)
149 else:
150 # If it's a generic type and we're unwrapping recursively
151 origin = get_origin(t)
152 if recursive and origin is not None and (args := get_args(t)):
153 unwrapped_args = tuple(_unwrap_all(arg, recursive) for arg in args)
154 # types.UnionType (PEP 604 X | Y) is not subscriptable, so we use _build_pep604_union_type
155 if origin is UnionType:
156 t = _build_pep604_union_type(list(unwrapped_args))
157 else:
158 t = origin[unwrapped_args]
160 return t
163def _is_variadic_type(t: type | UnionType) -> bool:
164 """Check if type is a Variadic or GreedyVariadic type."""
165 origin = get_origin(t)
166 if origin is Annotated:
167 args = get_args(t)
168 return len(args) >= 2 and args[1] in (HAYSTACK_VARIADIC_ANNOTATION, HAYSTACK_GREEDY_VARIADIC_ANNOTATION) # noqa: PLR2004
169 return False
172def _unwrap_variadics(t: type | UnionType, recursive: bool) -> type | UnionType:
173 """
174 Unwrap Variadic or GreedyVariadic annotated types.
176 :param t: Type to unwrap
177 :param recursive: If True, recursively unwraps nested types
178 :return: Unwrapped type if it was a variadic type, original type otherwise
179 """
180 if not _is_variadic_type(t):
181 return t
183 args = get_args(t)
184 # Get the Iterable[X] type and extract X
185 iterable_type = args[0]
186 inner_type = get_args(iterable_type)[0]
188 # Only recursively unwrap if requested
189 if recursive:
190 return _unwrap_all(inner_type, recursive)
191 return inner_type