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
« 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
5class FrozenDict(dict):
6 def __setitem__(self, key, value):
7 raise AttributeError("dict is frozen")
9 def __delitem__(self, key):
10 raise AttributeError("dict is frozen")
13class FrozenList(list):
14 def __setitem__(self, index, value):
15 raise AttributeError("list is frozen")
17 def __delitem__(self, index):
18 raise AttributeError("list is frozen")
20 def append(self, value):
21 raise AttributeError("list is frozen")
23 def extend(self, iterable):
24 raise AttributeError("list is frozen")
26 def insert(self, index, value):
27 raise AttributeError("list is frozen")
29 def remove(self, value):
30 raise AttributeError("list is frozen")
32 def pop(self, index=-1):
33 raise AttributeError("list is frozen")
35 def clear(self):
36 raise AttributeError("list is frozen")
39FreezeMe = TypeVar("FreezeMe")
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
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.
57 the [gelidum](https://github.com/diegojromerolopez/gelidum/) package is a more complete implementation of this idea
59 """
61 # mark as frozen
62 if hasattr(instance, "_IS_FROZEN"):
63 if instance._IS_FROZEN:
64 return instance
66 # try to mark as frozen
67 try:
68 instance._IS_FROZEN = True # type: ignore[attr-defined]
69 except AttributeError:
70 pass
72 # skip basic types, weird things, or already frozen things
73 if isinstance(instance, (bool, int, float, str, bytes)):
74 pass
76 elif isinstance(instance, (type(None), type(Ellipsis))):
77 pass
79 elif isinstance(instance, (FrozenList, FrozenDict, frozenset)):
80 pass
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)
88 elif isinstance(instance, tuple):
89 instance = tuple(freeze(item) for item in instance)
91 elif isinstance(instance, set):
92 instance = frozenset({freeze(item) for item in instance}) # type: ignore[assignment]
94 elif isinstance(instance, dict):
95 for key, value in instance.items():
96 instance[key] = freeze(value)
97 instance = FrozenDict(instance)
99 # handle custom classes
100 else:
101 # set everything in the __dict__ to frozen
102 instance.__dict__ = freeze(instance.__dict__) # type: ignore[assignment]
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")
109 FrozenClass.__name__ = f"FrozenClass__{instance.__class__.__name__}"
110 FrozenClass.__module__ = instance.__class__.__module__
111 FrozenClass.__doc__ = instance.__class__.__doc__
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
121 return instance