Source code for freeze

#
##
##  SPDX-FileCopyrightText: © 2007-2024 Benedict Verhegghe <bverheg@gmail.com>
##  SPDX-License-Identifier: GPL-3.0-or-later
##
##  This file is part of pyFormex 3.5  (Thu Feb  8 19:11:13 CET 2024)
##  pyFormex is a tool for generating, manipulating and transforming 3D
##  geometrical models by sequences of mathematical operations.
##  Home page: https://pyformex.org
##  Project page: https://savannah.nongnu.org/projects/pyformex/
##  Development: https://gitlab.com/bverheg/pyformex
##  Distributed under the GNU General Public License version 3 or later.
##
##  This program is free software: you can redistribute it and/or modify
##  it under the terms of the GNU General Public License as published by
##  the Free Software Foundation, either version 3 of the License, or
##  (at your option) any later version.
##
##  This program is distributed in the hope that it will be useful,
##  but WITHOUT ANY WARRANTY; without even the implied warranty of
##  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
##  GNU General Public License for more details.
##
##  You should have received a copy of the GNU General Public License
##  along with this program.  If not, see http://www.gnu.org/licenses/.
##
"""Freeze: create frozen objects

This module contains tools to create immutable objects, objects that
can not be changed. Python contains the following immutable ojects:
bool, int, float, str, tuple. The tuple is not completely immutable:
you can not add or remove items to it, but it may contain items that
can be changed. Then a read access to the item is enough to allow
changes to that item. This is a general observation for all container
types: to make the container contents immutable, the container class
itself must be frozen, but also all of the items contained in it.

This module provides two immutable container classes:
:class:`FrozenList` and :class:`FrozenDict`.
But it also provides methods to create your own frozen
classes and helps to freeze and thaw objects. Note that frozen objects
are not completely unchangeable: that is just impossible. But the data
are protected against inadvertent changes by the user. They must
explicitely be thawed to change them with normal methods.
Besides the :class:`FrozenList` and :class:`FrozenDict` classes,
this module provides the following functions:

- :func:`freeze`: make an object immutable
- :func:`thaw`: make a frozen objects mutable again
- :func:`frozen`: check if an object is immutable

Examples
--------
Create a frozen list and try to change it:

>>> FL = FrozenList([True, 1, 'abc'])
>>> FL[1] = 2
Traceback (most recent call last):
...
TypeError: 'FrozenList' object does not support item assignment

Same for a frozen dict:

>>> FD = FrozenDict({'a':1, 'b':'qwe'})
>>> del FD['a']
Traceback (most recent call last):
...
TypeError: 'FrozenDict' object does not support item deletion

:func:`freeze` and :func:`thaw` recursively freeze or thaw all items in the
containers:

>>> F = freeze([True, 1, 'abc', (0, 1), [1,2,3], {'a':0, 'b':[1,2]}])
>>> print(F)
FrozenList([True, 1, 'abc', (0, 1), FrozenList([1, 2, 3]), \
FrozenDict({'a': 0, 'b': FrozenList([1, 2])})])

>>> FT = freeze((0,1,[1,2]))
>>> print(FT)
FrozenTuple((0, 1, FrozenList([1, 2])))

Thawing a frozen object returns the original mutable object:

>>> thaw(FD)
{'a': 1, 'b': 'qwe'}
>>> thaw(F)
[True, 1, 'abc', (0, 1), [1, 2, 3], {'a': 0, 'b': [1, 2]}]
>>> thaw(FT)
(0, 1, [1, 2])

Create your own frozen classes
------------------------------
>>> class Point:
...     def __init__(self, x, y):
...         self.x = x
...         self.y = y
...     def __repr__(self):
...         return f"Point({self.x}, {self.y})"

>>> class FrozenPoint(tuple):
...     _frozen_ = True
...     def __new__(self, point):
...         return tuple.__new__(FrozenPoint, (freeze(P.x), freeze(P.y)))
...     def _thaw_(self):
...         return Point(*self)
...     def __repr__(self):
...         return "FrozenPoint" + tuple.__repr__(self)
>>> Freezer[Point] = FrozenPoint

>>> P = Point(1.4, 2.7)
>>> P.y = 3.5
>>> FP = freeze(P)
>>> FP
FrozenPoint(1.4, 3.5)
>>> FP[0]
1.4
>>> thaw(FP)
Point(1.4, 3.5)
"""

[docs]class FrozenTuple(tuple): """A frozen tuple A FrozenTuple is a tuple where each of the items is frozen. """ _frozen_ = True def __new__(self, data, /): if isinstance(data, tuple): return tuple.__new__(FrozenTuple, [freeze(i) for i in data]) else: raise ValueError("data should be a tuple of freezable items") def _thaw_(self): return tuple(thaw(i) for i in self) @property def _data_(self): """Return data as a tuple""" return tuple(self) def __repr__(self): return f"{self.__class__.__name__}({self._data_})"
[docs]class FrozenList(tuple): """A frozen list""" _frozen_ = True def __new__(self, data, /): if isinstance(data, list): return tuple.__new__(FrozenList, [freeze(i) for i in data]) else: raise ValueError("data should be list of freezable items") def _thaw_(self): return list(thaw(i) for i in self) @property def _data_(self): """Return data as a list""" return list(self) def __repr__(self): return f"{self.__class__.__name__}({self._data_})"
[docs]class FrozenDict(tuple): """A frozen dict""" _frozen_ = True def __new__(self, data, /): if isinstance(data, dict): return tuple.__new__(FrozenDict, [ (k, v if frozen(v) else freeze(v)) for k, v in data.items()]) else: raise ValueError("data should be a dict of freezable items") def __init__(self, *args, **kargs): self._index = dict([(i[0], j) for j, i in enumerate(self)]) def _thaw_(self): return dict([(k, thaw(v)) for k, v in self]) @property def _data_(self): return dict(self) def __repr__(self): return f"{self.__class__.__name__}({self._data_})" def __getitem__(self, key): i = self._index[key] print(f"{i=}") return tuple.__getitem__(self, i)[1]
Freezer = { tuple: FrozenTuple, list: FrozenList, dict: FrozenDict, }
[docs]def frozen(obj): """Check that an object is immutable""" return ( isinstance(obj, (int, float, str)) or hasattr(obj, '_frozen_') and obj._frozen_ or isinstance(obj, tuple) and all(frozen(o) for o in obj) )
def freeze(obj): if frozen(obj): return obj frozen_class = Freezer.get(obj.__class__, None) if frozen_class: return frozen_class(obj) raise TypeError(f"Can not freeze object of type {type(obj)}") def thaw(obj): if hasattr(obj, '_thaw_'): return obj._thaw_() return obj
[docs]def freezable(obj): """Check if an object is freezable""" return frozen(obj) or obj.__class__ in Freezer
if __name__ == '__main__': import doctest doctest.testmod( optionflags=doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS) # End