Coverage for haystack/utils/dataclasses.py: 100%
22 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
5import warnings
6from functools import wraps
7from typing import Any, TypeVar
9T = TypeVar("T")
12def _warn_on_inplace_mutation(cls: T) -> T:
13 """
14 Decorator that warns if the dataclass is mutated in-place.
15 """
16 initializing = set()
18 # mypy requires using getattr/setattr for dunder access, but ruff prefers
19 # direct attribute access. We silence mypy here in favor of the more explicit syntax.
20 original_init = cls.__init__ # type: ignore[misc]
21 original_setattr = cls.__setattr__
23 @wraps(original_init)
24 def __init_track__(self: T, *args: Any, **kwargs: Any) -> None:
25 # We don't raise warnings during initialization, i.e. during the first call to __init__ and __post_init__.
26 initializing.add(id(self))
27 try:
28 return original_init(self, *args, **kwargs)
29 finally:
30 initializing.discard(id(self))
32 @wraps(original_setattr)
33 def __setattr_warn__(self: T, name: str, value: Any) -> None:
34 # We raise warnings if the dataclass is mutated in-place after initialization.
35 if (
36 id(self) not in initializing
37 and name in getattr(self, "__dataclass_fields__", {})
38 and name in getattr(self, "__dict__", {})
39 ):
40 # We raise a warning if the attribute is a dataclass field and a dictionary key.
41 warnings.warn(
42 f"Mutating attribute '{name}' on an instance of "
43 f"'{type(self).__name__}' can lead to unexpected behavior by affecting other parts of the pipeline "
44 "that use the same dataclass instance. "
45 f"Use `dataclasses.replace(instance, {name}=new_value)` instead. "
46 "See https://docs.haystack.deepset.ai/docs/custom-components#requirements for details.",
47 Warning,
48 stacklevel=2,
49 )
50 # mypy infers original_setattr as bound to the type, expecting (str, Any), we call the unbound form
51 return original_setattr(self, name, value) # type: ignore[call-arg, arg-type]
53 # mypy considers direct dunder access on a class unsound, ruff prefers direct access
54 cls.__init__ = __init_track__ # type: ignore[misc]
55 # mypy does not allow assigning to a method, ruff prefers direct access
56 cls.__setattr__ = __setattr_warn__ # type: ignore[method-assign, assignment]
57 return cls