1 from __future__ import annotations

2

3

4 class 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

12 class 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

38 def 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