Coverage for muutils/misc/freezing.py: 89%

73 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-04-04 03:33 -0600

1from __future__ import annotations 

2from typing import Any, TypeVar, overload 

3 

4 

5class FrozenDict(dict): 

6 def __setitem__(self, key, value): 

7 raise AttributeError("dict is frozen") 

8 

9 def __delitem__(self, key): 

10 raise AttributeError("dict is frozen") 

11 

12 

13class FrozenList(list): 

14 def __setitem__(self, index, value): 

15 raise AttributeError("list is frozen") 

16 

17 def __delitem__(self, index): 

18 raise AttributeError("list is frozen") 

19 

20 def append(self, value): 

21 raise AttributeError("list is frozen") 

22 

23 def extend(self, iterable): 

24 raise AttributeError("list is frozen") 

25 

26 def insert(self, index, value): 

27 raise AttributeError("list is frozen") 

28 

29 def remove(self, value): 

30 raise AttributeError("list is frozen") 

31 

32 def pop(self, index=-1): 

33 raise AttributeError("list is frozen") 

34 

35 def clear(self): 

36 raise AttributeError("list is frozen") 

37 

38 

39FreezeMe = TypeVar("FreezeMe") 

40 

41 

42@overload 

43def freeze(instance: dict) -> FrozenDict: ... 

44@overload 

45def freeze(instance: list) -> FrozenList: ... 

46@overload 

47def freeze(instance: tuple) -> tuple: ... 

48@overload 

49def freeze(instance: set) -> frozenset: ... 

50@overload 

51def freeze(instance: FreezeMe) -> FreezeMe: ... 

52def freeze(instance: Any) -> Any: 

53 """recursively freeze an object in-place so that its attributes and elements cannot be changed 

54 

55 messy in the sense that sometimes the object is modified in place, but you can't rely on that. always use the return value. 

56 

57 the [gelidum](https://github.com/diegojromerolopez/gelidum/) package is a more complete implementation of this idea 

58 

59 """ 

60 

61 # mark as frozen 

62 if hasattr(instance, "_IS_FROZEN"): 

63 if instance._IS_FROZEN: 

64 return instance 

65 

66 # try to mark as frozen 

67 try: 

68 instance._IS_FROZEN = True # type: ignore[attr-defined] 

69 except AttributeError: 

70 pass 

71 

72 # skip basic types, weird things, or already frozen things 

73 if isinstance(instance, (bool, int, float, str, bytes)): 

74 pass 

75 

76 elif isinstance(instance, (type(None), type(Ellipsis))): 

77 pass 

78 

79 elif isinstance(instance, (FrozenList, FrozenDict, frozenset)): 

80 pass 

81 

82 # handle containers 

83 elif isinstance(instance, list): 

84 for i in range(len(instance)): 

85 instance[i] = freeze(instance[i]) 

86 instance = FrozenList(instance) 

87 

88 elif isinstance(instance, tuple): 

89 instance = tuple(freeze(item) for item in instance) 

90 

91 elif isinstance(instance, set): 

92 instance = frozenset({freeze(item) for item in instance}) # type: ignore[assignment] 

93 

94 elif isinstance(instance, dict): 

95 for key, value in instance.items(): 

96 instance[key] = freeze(value) 

97 instance = FrozenDict(instance) 

98 

99 # handle custom classes 

100 else: 

101 # set everything in the __dict__ to frozen 

102 instance.__dict__ = freeze(instance.__dict__) # type: ignore[assignment] 

103 

104 # create a new class which inherits from the original class 

105 class FrozenClass(instance.__class__): # type: ignore[name-defined] 

106 def __setattr__(self, name, value): 

107 raise AttributeError("class is frozen") 

108 

109 FrozenClass.__name__ = f"FrozenClass__{instance.__class__.__name__}" 

110 FrozenClass.__module__ = instance.__class__.__module__ 

111 FrozenClass.__doc__ = instance.__class__.__doc__ 

112 

113 # set the instance's class to the new class 

114 try: 

115 instance.__class__ = FrozenClass 

116 except TypeError as e: 

117 raise TypeError( 

118 f"Cannot freeze:\n{instance = }\n{instance.__class__ = }\n{FrozenClass = }" 

119 ) from e 

120 

121 return instance