#
##
## 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