Coverage for muutils\misc\freezing.py: 87%

61 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-12-12 20:43 -0700

1from __future__ import annotations 

2 

3 

4class FrozenDict(dict): 

5 def __setitem__(self, key, value): 

6 raise AttributeError("dict is frozen") 

7 

8 def __delitem__(self, key): 

9 raise AttributeError("dict is frozen") 

10 

11 

12class FrozenList(list): 

13 def __setitem__(self, index, value): 

14 raise AttributeError("list is frozen") 

15 

16 def __delitem__(self, index): 

17 raise AttributeError("list is frozen") 

18 

19 def append(self, value): 

20 raise AttributeError("list is frozen") 

21 

22 def extend(self, iterable): 

23 raise AttributeError("list is frozen") 

24 

25 def insert(self, index, value): 

26 raise AttributeError("list is frozen") 

27 

28 def remove(self, value): 

29 raise AttributeError("list is frozen") 

30 

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

32 raise AttributeError("list is frozen") 

33 

34 def clear(self): 

35 raise AttributeError("list is frozen") 

36 

37 

38def freeze(instance: object) -> object: 

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

40 

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

42 

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

44 

45 """ 

46 

47 # mark as frozen 

48 if hasattr(instance, "_IS_FROZEN"): 

49 if instance._IS_FROZEN: 

50 return instance 

51 

52 # try to mark as frozen 

53 try: 

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

55 except AttributeError: 

56 pass 

57 

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

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

60 pass 

61 

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

63 pass 

64 

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

66 pass 

67 

68 # handle containers 

69 elif isinstance(instance, list): 

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

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

72 instance = FrozenList(instance) 

73 

74 elif isinstance(instance, tuple): 

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

76 

77 elif isinstance(instance, set): 

78 instance = frozenset({freeze(item) for item in instance}) 

79 

80 elif isinstance(instance, dict): 

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

82 instance[key] = freeze(value) 

83 instance = FrozenDict(instance) 

84 

85 # handle custom classes 

86 else: 

87 # set everything in the __dict__ to frozen 

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

89 

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

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

92 def __setattr__(self, name, value): 

93 raise AttributeError("class is frozen") 

94 

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

96 FrozenClass.__module__ = instance.__class__.__module__ 

97 FrozenClass.__doc__ = instance.__class__.__doc__ 

98 

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

100 try: 

101 instance.__class__ = FrozenClass 

102 except TypeError as e: 

103 raise TypeError( 

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

105 ) from e 

106 

107 return instance