Coverage for muutils\misc\freezing.py: 87%
61 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-15 20:56 -0600
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-15 20:56 -0600
1from __future__ import annotations
4class FrozenDict(dict):
5 def __setitem__(self, key, value):
6 raise AttributeError("dict is frozen")
8 def __delitem__(self, key):
9 raise AttributeError("dict is frozen")
12class FrozenList(list):
13 def __setitem__(self, index, value):
14 raise AttributeError("list is frozen")
16 def __delitem__(self, index):
17 raise AttributeError("list is frozen")
19 def append(self, value):
20 raise AttributeError("list is frozen")
22 def extend(self, iterable):
23 raise AttributeError("list is frozen")
25 def insert(self, index, value):
26 raise AttributeError("list is frozen")
28 def remove(self, value):
29 raise AttributeError("list is frozen")
31 def pop(self, index=-1):
32 raise AttributeError("list is frozen")
34 def clear(self):
35 raise AttributeError("list is frozen")
38def freeze(instance: object) -> object:
39 """recursively freeze an object in-place so that its attributes and elements cannot be changed
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.
43 the [gelidum](https://github.com/diegojromerolopez/gelidum/) package is a more complete implementation of this idea
45 """
47 # mark as frozen
48 if hasattr(instance, "_IS_FROZEN"):
49 if instance._IS_FROZEN:
50 return instance
52 # try to mark as frozen
53 try:
54 instance._IS_FROZEN = True # type: ignore[attr-defined]
55 except AttributeError:
56 pass
58 # skip basic types, weird things, or already frozen things
59 if isinstance(instance, (bool, int, float, str, bytes)):
60 pass
62 elif isinstance(instance, (type(None), type(Ellipsis))):
63 pass
65 elif isinstance(instance, (FrozenList, FrozenDict, frozenset)):
66 pass
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)
74 elif isinstance(instance, tuple):
75 instance = tuple(freeze(item) for item in instance)
77 elif isinstance(instance, set):
78 instance = frozenset({freeze(item) for item in instance})
80 elif isinstance(instance, dict):
81 for key, value in instance.items():
82 instance[key] = freeze(value)
83 instance = FrozenDict(instance)
85 # handle custom classes
86 else:
87 # set everything in the __dict__ to frozen
88 instance.__dict__ = freeze(instance.__dict__) # type: ignore[assignment]
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")
95 FrozenClass.__name__ = f"FrozenClass__{instance.__class__.__name__}"
96 FrozenClass.__module__ = instance.__class__.__module__
97 FrozenClass.__doc__ = instance.__class__.__doc__
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
107 return instance