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

1# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai> 

2# 

3# SPDX-License-Identifier: Apache-2.0 

4 

5import warnings 

6from functools import wraps 

7from typing import Any, TypeVar 

8 

9T = TypeVar("T") 

10 

11 

12def _warn_on_inplace_mutation(cls: T) -> T: 

13 """ 

14 Decorator that warns if the dataclass is mutated in-place. 

15 """ 

16 initializing = set() 

17 

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__ 

22 

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)) 

31 

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] 

52 

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