diff options
author | CoprDistGit <infra@openeuler.org> | 2023-05-05 04:41:28 +0000 |
---|---|---|
committer | CoprDistGit <infra@openeuler.org> | 2023-05-05 04:41:28 +0000 |
commit | 662e531f22d6d1ec5a083395337add3538d84234 (patch) | |
tree | fde310116fd3f6d7316cddf236e3dfb25ea6d68b | |
parent | 059ecb1824c4fd9b13f8a41dbc3245a3a558874d (diff) |
automatic import of python-gelidumopeneuler20.03
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | python-gelidum.spec | 1263 | ||||
-rw-r--r-- | sources | 1 |
3 files changed, 1265 insertions, 0 deletions
@@ -0,0 +1 @@ +/gelidum-0.6.0.tar.gz diff --git a/python-gelidum.spec b/python-gelidum.spec new file mode 100644 index 0000000..15b0175 --- /dev/null +++ b/python-gelidum.spec @@ -0,0 +1,1263 @@ +%global _empty_manifest_terminate_build 0 +Name: python-gelidum +Version: 0.6.0 +Release: 1 +Summary: Freeze your python objects +License: MIT +URL: https://github.com/diegojromerolopez/gelidum +Source0: https://mirrors.nju.edu.cn/pypi/web/packages/e3/40/e8d6daafbf6776697e5ce8a663a59a07d70f3d4c65b60ba2c8d14fc8374b/gelidum-0.6.0.tar.gz +BuildArch: noarch + + +%description +# gelidum + + +[](https://opensource.org/licenses/MIT) +[](https://github.com/diegojromerolopez/gelidum/graphs/commit-activity) +[](https://www.python.org/) +[](https://pypi.python.org/pypi/gelidum/) +[](https://pypi.python.org/pypi/gelidum/) +[](https://pypi.python.org/pypi/gelidum/) +[](https://pypi.python.org/pypi/gelidum/) +[](https://codeclimate.com/github/diegojromerolopez/gelidum/maintainability) +[](https://codeclimate.com/github/diegojromerolopez/gelidum/test_coverage) + +Freeze your objects in python. + + + +[Public domain photo](https://www.flickr.com/photos/140296773@N07/45771062385) by [Katsujiro Maekawa](https://www.flickr.com/photos/hjppo/) + +| Latin | English | +| -------------------------------------------------------- | -------------------------------------------------------- | +| *Caelum est hieme frigidum et gelidum; myrtos oleas quaeque alia assiduo tepore laetantur, aspernatur ac respuit; laurum tamen patitur atque etiam nitidissimam profert, interdum sed non saepius quam sub urbe nostra necat.* | *In winter the air is cold and frosty: myrtles, olives and all other trees which require constant warmth for them to do well, the climate rejects and spurns, though it allows laurel to grow, and even brings it to a luxuriant leaf. Occasionally, however, it kills it, but that does not happen more frequently than in the neighbourhood of Rome.* | + +[The Letters of the Younger Pliny, First Series — Volume 1 by the Younger Pliny](https://www.gutenberg.org/ebooks/3234), translated to English by John Benjamin Firth. + +## Introduction +Inspired by the method freeze found in other languages like Javascript, +this package tries to make immutable objects to make it easier avoiding +accidental modifications in your code. + +See more comments about this project in this [Show HN](https://news.ycombinator.com/item?id=27507524). + +## Major highlights +- **freeze** method creates objects with the same attributes of inputs that cannot be expanded or modified. +- Frozen object creation is thread-safe. +- Structural sharing: any frozen object is shared by all of its user objects. There is no copy +performed, only reference. +- cpython and pypy support. + +## How it works +In case of the [builtin types](https://docs.python.org/3/library/stdtypes.html) +(bool, None, int, float, bytes, complex, str) it does nothing, as they are already immutable. + +For the list type, a [frozenlist](/gelidum/collections/frozenlist.py) with frozen items is returned. + +Tuples are already immutable, so a new tuple with frozen items is returned. + +When freezing a set, a [frozenzet](/gelidum/collections/frozenzet.py) of frozen items is returned. + +In the case of dicts, freezing one of them creates +a new [frozendict](/gelidum/collections/frozendict.py) +with the keys and frozen values of the original dict. + +This package, change the methods \_\_setattr\_\_, \_\_delattr\_\_, \_\_set\_\_, +\_\_setitem\_\_, and \_\_delitem\_\_ of the object argument and all of its attributed recursively, +making them raise an exception if the developer tries to call them to modify +the attributes of the instance. + +## How to use it + +### Freeze in the same object +```python +from typing import List +from gelidum import freeze + +class Dummy(object): + def __init__(self, attr1: int, attr2: List): + self.attr1 = attr1 + self.attr2 = attr2 + +dummy = Dummy(1, [2, 3, 4]) +frozen_dummy = freeze(dummy, on_freeze="inplace") +assert(id(dummy) == id(frozen_dummy)) + +# Both raise exception +new_value = 1 +dummy.attr1 = new_value +frozen_dummy.attr1 = new_value + +# Both raise exception +new_value_list = [1] +dummy.attr2 = new_value_list +frozen_dummy.attr2 = new_value_list +``` + +### Freeze in a new object + +#### Basic use +```python +from typing import List +from gelidum import freeze + +class Dummy(object): + def __init__(self, attr1: int, attr2: List): + self.attr1 = attr1 + self.attr2 = attr2 + +dummy = Dummy(1, [2, 3, 4]) +# on_freeze="copy" by default +frozen_dummy = freeze(dummy) +assert(id(dummy) != id(frozen_dummy)) + +# on_freeze="copy" by default +frozen_object_dummy2 = freeze(dummy, on_freeze="copy") + +# It doesn't raise an exception, +# dummy keeps being a mutable object +new_attr1_value = 99 +dummy.attr1 = new_attr1_value + +# Raises exception, +# frozen_dummy is an immutable object +frozen_dummy.attr1 = new_attr1_value +``` + +#### Access to original object +The parameter on_freeze admits a callable, so you can have +some side effects when freezing objects. + +There is a particular callable class that allows +returning the original object: + +```python +from gelidum import freeze +from gelidum.on_freeze import OnFreezeOriginalObjTracker + +class Dummy(object): + def __init__(self, value1: int, value2: int): + self.attr1 = value1 + self.attr2 = value2 + +dummy = Dummy(value1=1, value2=2) + +freezer = OnFreezeOriginalObjTracker() + +frozen_dummy = freeze(dummy, on_freeze=freezer) +original_obj = freezer.original_obj + +assert(dummy == original_obj) +``` + +Note that in the earlier case the original object is not frozen +but a copy of it. + +#### What to do when trying to update an attribute +```python +import logging +from gelidum import freeze + +class SharedState(object): + def __init__(self, count: int): + self.count = count + +shared_state = SharedState(1) + +# on_update="exception": raises an exception when an update is tried +frozen_shared_state = freeze(shared_state, on_update="exception") +frozen_shared_state.count = 4 # Raises exception + +# on_update="warning": shows a warning in console exception when an update is tried +frozen_shared_state = freeze(shared_state, on_update="warning") +frozen_shared_state.count = 4 # Shows a warning in console + +# on_update="nothing": does nothing when an update is tried +frozen_shared_state = freeze(shared_state, on_update="nothing") +frozen_shared_state.count = 4 # Does nothing, as this update did not exist + +# on_update=<lambda message, *args, **kwargs>: calls the function +# Note the parameters of that function must be message, *args, **kwargs +frozen_shared_state = freeze( + shared_state, + on_update=lambda message, *args, **kwargs: logging.warning(message) +) +frozen_shared_state.count = 4 # Calls on_update function and logs in the warning level: + # "Can't assign 'count' on immutable instance" +``` + + +### Freeze input params +Use the decorator freeze_params to freeze the input parameters +and avoid non-intended modifications: +```python +from typing import List +from gelidum import freeze_params + +@freeze_params() +def append_to_list(a_list: List, new_item: int): + a_list.append(new_item) +``` +If freeze_params is called without arguments, all input parameters will be frozen. +Otherwise, passing a set of parameters will inform the decorator of which named +parameters must be frozen. + +```python +from typing import List +from gelidum import freeze_params + +@freeze_params(params={"list1", "list2"}) +def concat_lists(dest: List, list1: List, list2: List) -> List: + dest = list1 + list2 + return dest + +# Freeze dest, list1 and list2 +concat_lists([], list1=[1, 2, 3], list2=[4, 5, 6]) + +# Freeze list1 and list2 +concat_lists(dest=[], list1=[1, 2, 3], list2=[4, 5, 6]) +``` + +Always use kwargs unless you want to freeze the args params. A good way to enforce this is by making the +function have keyword-only arguments: + +```python +from typing import List +from gelidum import freeze_params + +@freeze_params(params={"list1", "list2"}) +def concat_lists_in(*, dest: List, list1: List, list2: List): + dest = list1 + list2 + return dest +``` + +You can use the **Final typehint from gelidum** to signal that an argument is immutable: + +```python +from typing import List +from gelidum import freeze_final, Final + +@freeze_final +def concatenate_lists(list1: Final[List], list2: Final[List]): + return list1 + list2 +``` + +Finally, take in account that all freezing is done in a new object (i.e. freeze with on_freeze="copy"). +It makes no sense to freeze a parameter of a function that could be used later, *outside* +said function. + +### Check original (i.e. "hot") class +- **get_gelidum_hot_class_name**: returns the name of hot class. +- **get_gelidum_hot_class_module** returns the module reference where the hot class was. + +## Collections +There are four immutable collections in the gelidum.collections module. + +- frozendict +- frozenlist +- frozenzet (frozenset is already a builtin type in Python) + +All of these classes can be used to make sure a collection of objects +is not modified. Indeed, when creating a new collection object, you +can pass a custom freeze function, to customize the freezing process +of each of its items, e.g.: + +```python +import logging +from gelidum.freeze import freeze +from gelidum.collections import frozenzet +from gelidum.typing import FrozenType +from typing import Any + + +def my_freeze_func(item: Any) -> FrozenType: + logging.debug(f"Freezing item {item}") + return freeze(item, on_update="exception", on_freeze="copy") + +frozen_zet = frozenzet([1, 2, 3], freeze_func=my_freeze_func) +``` + +## Rationale and background information +Inspired by my old work with Ruby on Rails, I decided to create a mechanism to make +objects immutable in Python. The first aim was to do a tool to avoid accidental +modifications on the objects while passing them through an execution flow. + +Anyways, as time passed I thought that an implementation of a programming language +with real threading support (i.e. not cpython) could be benefited from this feature. +I know that both cpython and pypy implementations of the Python programming +language have a [GIL](https://en.wikipedia.org/wiki/Global_interpreter_lock) but IronPython +and [Graalpython](https://github.com/oracle/graalpython) don't. +IronPython3 has no support for typehintings yet, +but Graalpython seems to work fine, so more experiments will be coming. + +On the other hand, I'm also interested in creating functional data structures +in this package, easing the life of developers that do not want side effects. + +It's true that the complexity of Python does not play well with this kind of library. +Thus, Python usually serves as easy interface with native libraries (pandas, numpy, etc.) +However, this project is fun to develop and maybe with the popularity of alternative +implementations of Python some work can be done to improve performance. + +More information can be seen in this [Show HN post](https://news.ycombinator.com/item?id=27507524) +and some appreciated feedback of the users of that great community. + +## Limitations +- dict, list, tuple and set objects cannot be modified inplace although the flag inplace is set. +- file handler attributes are not supported. An exception is raised when trying to freeze + an object with them. +- frozen objects cannot be serialized with [marshal](https://docs.python.org/3/library/marshal.html). +- frozen objects cannot be (deep)-copied. This limitation is intended to make structural sharing easier. +- Classes with \_\_slots\_\_: + - cannot be frozen in-place. + - will be frozen with a unique class. The frozen class will not be shared by instances of the same class. + +## Advice & comments on use +### On_update parameter of freeze function +Use on_update with a callable to store when somebody tried to write in the immutable object: +```python +import datetime +import logging +import threading +from gelidum import freeze + + +class Dummy(object): + def __init__(self, attr: int): + self.attr = attr + + +class FrozenDummyUpdateTryRecorder: + LOCK = threading.Lock() + written_tries = [] + + @classmethod + def add_writing_try(cls, message, *args, **kwargs): + logging.warning(message) + with cls.LOCK: + cls.written_tries.append({ + "message": message, + "args": args, + "kwargs": kwargs, + "datetime": datetime.datetime.utcnow() + }) + + +dummy = Dummy(1) +frozen_dummy = freeze( + dummy, + on_update=FrozenDummyUpdateTryRecorder.add_writing_try + ) +# It will call FrozenDummyUpdateTryRecorder.add_writing_try +# and will continue the execution flow with the next sentence. +frozen_dummy.attr = 4 +``` + +### On_freeze parameter of freeze function +The parameter on_freeze of the function freeze must be a string or a callable. +This parameter informs of what to do with the object that will be frozen. +Should it be the same input object frozen or a copy of it? + +If it has a string as parameter, values "inplace" and "copy" are allowed. +A value of "inplace" will make the freeze method to try to freeze the object +as-is, while a value of "copy" will make a copy of the original object and then, +freeze that copy. **These are the recommended parameters**. + +On the other hand, the interesting part is to define a custom on_freeze method. +This method must return an object of the same type of the input. +**This returned will be frozen, and returned to the caller of freeze**. + +Note this parameter has no interference with the structural sharing of the frozen objects. +Any frozen object that have several references to it will be shared, not copied. + +```python +import copy + +def on_freeze(self, obj: object) -> object: + frozen_object = copy.deepcopy(obj) + # log, copy the original method or do any other + # custom action in this function + return frozen_object +``` +As seen earlier, there is also the possibility to +pass a callable object. If you would like you can even +define your own on_freeze functions by inheriting +from classes: + +- OnFreezeCopier +- OnFreezeIdentityFunc + +See some examples in [on_freeze.py](/gelidum/on_freeze.py) file. + +## Dependencies +This package has no dependencies. + + +## Roadmap +- [x] Freeze only when attributes are modified? + Not exactly but structural sharing is used. +- [x] Include immutable collections. +- [ ] [Graalpython](https://github.com/oracle/graalpython) support. +- [ ] Make some use-cases with threading/async module (i.e. server) + + +## Collaborations +This project is open to collaborations. Make a PR or an issue, +and I'll take a look to it. + +## License +[MIT](LICENSE) license, but if you need any other contact me. + + +%package -n python3-gelidum +Summary: Freeze your python objects +Provides: python-gelidum +BuildRequires: python3-devel +BuildRequires: python3-setuptools +BuildRequires: python3-pip +%description -n python3-gelidum +# gelidum + + +[](https://opensource.org/licenses/MIT) +[](https://github.com/diegojromerolopez/gelidum/graphs/commit-activity) +[](https://www.python.org/) +[](https://pypi.python.org/pypi/gelidum/) +[](https://pypi.python.org/pypi/gelidum/) +[](https://pypi.python.org/pypi/gelidum/) +[](https://pypi.python.org/pypi/gelidum/) +[](https://codeclimate.com/github/diegojromerolopez/gelidum/maintainability) +[](https://codeclimate.com/github/diegojromerolopez/gelidum/test_coverage) + +Freeze your objects in python. + + + +[Public domain photo](https://www.flickr.com/photos/140296773@N07/45771062385) by [Katsujiro Maekawa](https://www.flickr.com/photos/hjppo/) + +| Latin | English | +| -------------------------------------------------------- | -------------------------------------------------------- | +| *Caelum est hieme frigidum et gelidum; myrtos oleas quaeque alia assiduo tepore laetantur, aspernatur ac respuit; laurum tamen patitur atque etiam nitidissimam profert, interdum sed non saepius quam sub urbe nostra necat.* | *In winter the air is cold and frosty: myrtles, olives and all other trees which require constant warmth for them to do well, the climate rejects and spurns, though it allows laurel to grow, and even brings it to a luxuriant leaf. Occasionally, however, it kills it, but that does not happen more frequently than in the neighbourhood of Rome.* | + +[The Letters of the Younger Pliny, First Series — Volume 1 by the Younger Pliny](https://www.gutenberg.org/ebooks/3234), translated to English by John Benjamin Firth. + +## Introduction +Inspired by the method freeze found in other languages like Javascript, +this package tries to make immutable objects to make it easier avoiding +accidental modifications in your code. + +See more comments about this project in this [Show HN](https://news.ycombinator.com/item?id=27507524). + +## Major highlights +- **freeze** method creates objects with the same attributes of inputs that cannot be expanded or modified. +- Frozen object creation is thread-safe. +- Structural sharing: any frozen object is shared by all of its user objects. There is no copy +performed, only reference. +- cpython and pypy support. + +## How it works +In case of the [builtin types](https://docs.python.org/3/library/stdtypes.html) +(bool, None, int, float, bytes, complex, str) it does nothing, as they are already immutable. + +For the list type, a [frozenlist](/gelidum/collections/frozenlist.py) with frozen items is returned. + +Tuples are already immutable, so a new tuple with frozen items is returned. + +When freezing a set, a [frozenzet](/gelidum/collections/frozenzet.py) of frozen items is returned. + +In the case of dicts, freezing one of them creates +a new [frozendict](/gelidum/collections/frozendict.py) +with the keys and frozen values of the original dict. + +This package, change the methods \_\_setattr\_\_, \_\_delattr\_\_, \_\_set\_\_, +\_\_setitem\_\_, and \_\_delitem\_\_ of the object argument and all of its attributed recursively, +making them raise an exception if the developer tries to call them to modify +the attributes of the instance. + +## How to use it + +### Freeze in the same object +```python +from typing import List +from gelidum import freeze + +class Dummy(object): + def __init__(self, attr1: int, attr2: List): + self.attr1 = attr1 + self.attr2 = attr2 + +dummy = Dummy(1, [2, 3, 4]) +frozen_dummy = freeze(dummy, on_freeze="inplace") +assert(id(dummy) == id(frozen_dummy)) + +# Both raise exception +new_value = 1 +dummy.attr1 = new_value +frozen_dummy.attr1 = new_value + +# Both raise exception +new_value_list = [1] +dummy.attr2 = new_value_list +frozen_dummy.attr2 = new_value_list +``` + +### Freeze in a new object + +#### Basic use +```python +from typing import List +from gelidum import freeze + +class Dummy(object): + def __init__(self, attr1: int, attr2: List): + self.attr1 = attr1 + self.attr2 = attr2 + +dummy = Dummy(1, [2, 3, 4]) +# on_freeze="copy" by default +frozen_dummy = freeze(dummy) +assert(id(dummy) != id(frozen_dummy)) + +# on_freeze="copy" by default +frozen_object_dummy2 = freeze(dummy, on_freeze="copy") + +# It doesn't raise an exception, +# dummy keeps being a mutable object +new_attr1_value = 99 +dummy.attr1 = new_attr1_value + +# Raises exception, +# frozen_dummy is an immutable object +frozen_dummy.attr1 = new_attr1_value +``` + +#### Access to original object +The parameter on_freeze admits a callable, so you can have +some side effects when freezing objects. + +There is a particular callable class that allows +returning the original object: + +```python +from gelidum import freeze +from gelidum.on_freeze import OnFreezeOriginalObjTracker + +class Dummy(object): + def __init__(self, value1: int, value2: int): + self.attr1 = value1 + self.attr2 = value2 + +dummy = Dummy(value1=1, value2=2) + +freezer = OnFreezeOriginalObjTracker() + +frozen_dummy = freeze(dummy, on_freeze=freezer) +original_obj = freezer.original_obj + +assert(dummy == original_obj) +``` + +Note that in the earlier case the original object is not frozen +but a copy of it. + +#### What to do when trying to update an attribute +```python +import logging +from gelidum import freeze + +class SharedState(object): + def __init__(self, count: int): + self.count = count + +shared_state = SharedState(1) + +# on_update="exception": raises an exception when an update is tried +frozen_shared_state = freeze(shared_state, on_update="exception") +frozen_shared_state.count = 4 # Raises exception + +# on_update="warning": shows a warning in console exception when an update is tried +frozen_shared_state = freeze(shared_state, on_update="warning") +frozen_shared_state.count = 4 # Shows a warning in console + +# on_update="nothing": does nothing when an update is tried +frozen_shared_state = freeze(shared_state, on_update="nothing") +frozen_shared_state.count = 4 # Does nothing, as this update did not exist + +# on_update=<lambda message, *args, **kwargs>: calls the function +# Note the parameters of that function must be message, *args, **kwargs +frozen_shared_state = freeze( + shared_state, + on_update=lambda message, *args, **kwargs: logging.warning(message) +) +frozen_shared_state.count = 4 # Calls on_update function and logs in the warning level: + # "Can't assign 'count' on immutable instance" +``` + + +### Freeze input params +Use the decorator freeze_params to freeze the input parameters +and avoid non-intended modifications: +```python +from typing import List +from gelidum import freeze_params + +@freeze_params() +def append_to_list(a_list: List, new_item: int): + a_list.append(new_item) +``` +If freeze_params is called without arguments, all input parameters will be frozen. +Otherwise, passing a set of parameters will inform the decorator of which named +parameters must be frozen. + +```python +from typing import List +from gelidum import freeze_params + +@freeze_params(params={"list1", "list2"}) +def concat_lists(dest: List, list1: List, list2: List) -> List: + dest = list1 + list2 + return dest + +# Freeze dest, list1 and list2 +concat_lists([], list1=[1, 2, 3], list2=[4, 5, 6]) + +# Freeze list1 and list2 +concat_lists(dest=[], list1=[1, 2, 3], list2=[4, 5, 6]) +``` + +Always use kwargs unless you want to freeze the args params. A good way to enforce this is by making the +function have keyword-only arguments: + +```python +from typing import List +from gelidum import freeze_params + +@freeze_params(params={"list1", "list2"}) +def concat_lists_in(*, dest: List, list1: List, list2: List): + dest = list1 + list2 + return dest +``` + +You can use the **Final typehint from gelidum** to signal that an argument is immutable: + +```python +from typing import List +from gelidum import freeze_final, Final + +@freeze_final +def concatenate_lists(list1: Final[List], list2: Final[List]): + return list1 + list2 +``` + +Finally, take in account that all freezing is done in a new object (i.e. freeze with on_freeze="copy"). +It makes no sense to freeze a parameter of a function that could be used later, *outside* +said function. + +### Check original (i.e. "hot") class +- **get_gelidum_hot_class_name**: returns the name of hot class. +- **get_gelidum_hot_class_module** returns the module reference where the hot class was. + +## Collections +There are four immutable collections in the gelidum.collections module. + +- frozendict +- frozenlist +- frozenzet (frozenset is already a builtin type in Python) + +All of these classes can be used to make sure a collection of objects +is not modified. Indeed, when creating a new collection object, you +can pass a custom freeze function, to customize the freezing process +of each of its items, e.g.: + +```python +import logging +from gelidum.freeze import freeze +from gelidum.collections import frozenzet +from gelidum.typing import FrozenType +from typing import Any + + +def my_freeze_func(item: Any) -> FrozenType: + logging.debug(f"Freezing item {item}") + return freeze(item, on_update="exception", on_freeze="copy") + +frozen_zet = frozenzet([1, 2, 3], freeze_func=my_freeze_func) +``` + +## Rationale and background information +Inspired by my old work with Ruby on Rails, I decided to create a mechanism to make +objects immutable in Python. The first aim was to do a tool to avoid accidental +modifications on the objects while passing them through an execution flow. + +Anyways, as time passed I thought that an implementation of a programming language +with real threading support (i.e. not cpython) could be benefited from this feature. +I know that both cpython and pypy implementations of the Python programming +language have a [GIL](https://en.wikipedia.org/wiki/Global_interpreter_lock) but IronPython +and [Graalpython](https://github.com/oracle/graalpython) don't. +IronPython3 has no support for typehintings yet, +but Graalpython seems to work fine, so more experiments will be coming. + +On the other hand, I'm also interested in creating functional data structures +in this package, easing the life of developers that do not want side effects. + +It's true that the complexity of Python does not play well with this kind of library. +Thus, Python usually serves as easy interface with native libraries (pandas, numpy, etc.) +However, this project is fun to develop and maybe with the popularity of alternative +implementations of Python some work can be done to improve performance. + +More information can be seen in this [Show HN post](https://news.ycombinator.com/item?id=27507524) +and some appreciated feedback of the users of that great community. + +## Limitations +- dict, list, tuple and set objects cannot be modified inplace although the flag inplace is set. +- file handler attributes are not supported. An exception is raised when trying to freeze + an object with them. +- frozen objects cannot be serialized with [marshal](https://docs.python.org/3/library/marshal.html). +- frozen objects cannot be (deep)-copied. This limitation is intended to make structural sharing easier. +- Classes with \_\_slots\_\_: + - cannot be frozen in-place. + - will be frozen with a unique class. The frozen class will not be shared by instances of the same class. + +## Advice & comments on use +### On_update parameter of freeze function +Use on_update with a callable to store when somebody tried to write in the immutable object: +```python +import datetime +import logging +import threading +from gelidum import freeze + + +class Dummy(object): + def __init__(self, attr: int): + self.attr = attr + + +class FrozenDummyUpdateTryRecorder: + LOCK = threading.Lock() + written_tries = [] + + @classmethod + def add_writing_try(cls, message, *args, **kwargs): + logging.warning(message) + with cls.LOCK: + cls.written_tries.append({ + "message": message, + "args": args, + "kwargs": kwargs, + "datetime": datetime.datetime.utcnow() + }) + + +dummy = Dummy(1) +frozen_dummy = freeze( + dummy, + on_update=FrozenDummyUpdateTryRecorder.add_writing_try + ) +# It will call FrozenDummyUpdateTryRecorder.add_writing_try +# and will continue the execution flow with the next sentence. +frozen_dummy.attr = 4 +``` + +### On_freeze parameter of freeze function +The parameter on_freeze of the function freeze must be a string or a callable. +This parameter informs of what to do with the object that will be frozen. +Should it be the same input object frozen or a copy of it? + +If it has a string as parameter, values "inplace" and "copy" are allowed. +A value of "inplace" will make the freeze method to try to freeze the object +as-is, while a value of "copy" will make a copy of the original object and then, +freeze that copy. **These are the recommended parameters**. + +On the other hand, the interesting part is to define a custom on_freeze method. +This method must return an object of the same type of the input. +**This returned will be frozen, and returned to the caller of freeze**. + +Note this parameter has no interference with the structural sharing of the frozen objects. +Any frozen object that have several references to it will be shared, not copied. + +```python +import copy + +def on_freeze(self, obj: object) -> object: + frozen_object = copy.deepcopy(obj) + # log, copy the original method or do any other + # custom action in this function + return frozen_object +``` +As seen earlier, there is also the possibility to +pass a callable object. If you would like you can even +define your own on_freeze functions by inheriting +from classes: + +- OnFreezeCopier +- OnFreezeIdentityFunc + +See some examples in [on_freeze.py](/gelidum/on_freeze.py) file. + +## Dependencies +This package has no dependencies. + + +## Roadmap +- [x] Freeze only when attributes are modified? + Not exactly but structural sharing is used. +- [x] Include immutable collections. +- [ ] [Graalpython](https://github.com/oracle/graalpython) support. +- [ ] Make some use-cases with threading/async module (i.e. server) + + +## Collaborations +This project is open to collaborations. Make a PR or an issue, +and I'll take a look to it. + +## License +[MIT](LICENSE) license, but if you need any other contact me. + + +%package help +Summary: Development documents and examples for gelidum +Provides: python3-gelidum-doc +%description help +# gelidum + + +[](https://opensource.org/licenses/MIT) +[](https://github.com/diegojromerolopez/gelidum/graphs/commit-activity) +[](https://www.python.org/) +[](https://pypi.python.org/pypi/gelidum/) +[](https://pypi.python.org/pypi/gelidum/) +[](https://pypi.python.org/pypi/gelidum/) +[](https://pypi.python.org/pypi/gelidum/) +[](https://codeclimate.com/github/diegojromerolopez/gelidum/maintainability) +[](https://codeclimate.com/github/diegojromerolopez/gelidum/test_coverage) + +Freeze your objects in python. + + + +[Public domain photo](https://www.flickr.com/photos/140296773@N07/45771062385) by [Katsujiro Maekawa](https://www.flickr.com/photos/hjppo/) + +| Latin | English | +| -------------------------------------------------------- | -------------------------------------------------------- | +| *Caelum est hieme frigidum et gelidum; myrtos oleas quaeque alia assiduo tepore laetantur, aspernatur ac respuit; laurum tamen patitur atque etiam nitidissimam profert, interdum sed non saepius quam sub urbe nostra necat.* | *In winter the air is cold and frosty: myrtles, olives and all other trees which require constant warmth for them to do well, the climate rejects and spurns, though it allows laurel to grow, and even brings it to a luxuriant leaf. Occasionally, however, it kills it, but that does not happen more frequently than in the neighbourhood of Rome.* | + +[The Letters of the Younger Pliny, First Series — Volume 1 by the Younger Pliny](https://www.gutenberg.org/ebooks/3234), translated to English by John Benjamin Firth. + +## Introduction +Inspired by the method freeze found in other languages like Javascript, +this package tries to make immutable objects to make it easier avoiding +accidental modifications in your code. + +See more comments about this project in this [Show HN](https://news.ycombinator.com/item?id=27507524). + +## Major highlights +- **freeze** method creates objects with the same attributes of inputs that cannot be expanded or modified. +- Frozen object creation is thread-safe. +- Structural sharing: any frozen object is shared by all of its user objects. There is no copy +performed, only reference. +- cpython and pypy support. + +## How it works +In case of the [builtin types](https://docs.python.org/3/library/stdtypes.html) +(bool, None, int, float, bytes, complex, str) it does nothing, as they are already immutable. + +For the list type, a [frozenlist](/gelidum/collections/frozenlist.py) with frozen items is returned. + +Tuples are already immutable, so a new tuple with frozen items is returned. + +When freezing a set, a [frozenzet](/gelidum/collections/frozenzet.py) of frozen items is returned. + +In the case of dicts, freezing one of them creates +a new [frozendict](/gelidum/collections/frozendict.py) +with the keys and frozen values of the original dict. + +This package, change the methods \_\_setattr\_\_, \_\_delattr\_\_, \_\_set\_\_, +\_\_setitem\_\_, and \_\_delitem\_\_ of the object argument and all of its attributed recursively, +making them raise an exception if the developer tries to call them to modify +the attributes of the instance. + +## How to use it + +### Freeze in the same object +```python +from typing import List +from gelidum import freeze + +class Dummy(object): + def __init__(self, attr1: int, attr2: List): + self.attr1 = attr1 + self.attr2 = attr2 + +dummy = Dummy(1, [2, 3, 4]) +frozen_dummy = freeze(dummy, on_freeze="inplace") +assert(id(dummy) == id(frozen_dummy)) + +# Both raise exception +new_value = 1 +dummy.attr1 = new_value +frozen_dummy.attr1 = new_value + +# Both raise exception +new_value_list = [1] +dummy.attr2 = new_value_list +frozen_dummy.attr2 = new_value_list +``` + +### Freeze in a new object + +#### Basic use +```python +from typing import List +from gelidum import freeze + +class Dummy(object): + def __init__(self, attr1: int, attr2: List): + self.attr1 = attr1 + self.attr2 = attr2 + +dummy = Dummy(1, [2, 3, 4]) +# on_freeze="copy" by default +frozen_dummy = freeze(dummy) +assert(id(dummy) != id(frozen_dummy)) + +# on_freeze="copy" by default +frozen_object_dummy2 = freeze(dummy, on_freeze="copy") + +# It doesn't raise an exception, +# dummy keeps being a mutable object +new_attr1_value = 99 +dummy.attr1 = new_attr1_value + +# Raises exception, +# frozen_dummy is an immutable object +frozen_dummy.attr1 = new_attr1_value +``` + +#### Access to original object +The parameter on_freeze admits a callable, so you can have +some side effects when freezing objects. + +There is a particular callable class that allows +returning the original object: + +```python +from gelidum import freeze +from gelidum.on_freeze import OnFreezeOriginalObjTracker + +class Dummy(object): + def __init__(self, value1: int, value2: int): + self.attr1 = value1 + self.attr2 = value2 + +dummy = Dummy(value1=1, value2=2) + +freezer = OnFreezeOriginalObjTracker() + +frozen_dummy = freeze(dummy, on_freeze=freezer) +original_obj = freezer.original_obj + +assert(dummy == original_obj) +``` + +Note that in the earlier case the original object is not frozen +but a copy of it. + +#### What to do when trying to update an attribute +```python +import logging +from gelidum import freeze + +class SharedState(object): + def __init__(self, count: int): + self.count = count + +shared_state = SharedState(1) + +# on_update="exception": raises an exception when an update is tried +frozen_shared_state = freeze(shared_state, on_update="exception") +frozen_shared_state.count = 4 # Raises exception + +# on_update="warning": shows a warning in console exception when an update is tried +frozen_shared_state = freeze(shared_state, on_update="warning") +frozen_shared_state.count = 4 # Shows a warning in console + +# on_update="nothing": does nothing when an update is tried +frozen_shared_state = freeze(shared_state, on_update="nothing") +frozen_shared_state.count = 4 # Does nothing, as this update did not exist + +# on_update=<lambda message, *args, **kwargs>: calls the function +# Note the parameters of that function must be message, *args, **kwargs +frozen_shared_state = freeze( + shared_state, + on_update=lambda message, *args, **kwargs: logging.warning(message) +) +frozen_shared_state.count = 4 # Calls on_update function and logs in the warning level: + # "Can't assign 'count' on immutable instance" +``` + + +### Freeze input params +Use the decorator freeze_params to freeze the input parameters +and avoid non-intended modifications: +```python +from typing import List +from gelidum import freeze_params + +@freeze_params() +def append_to_list(a_list: List, new_item: int): + a_list.append(new_item) +``` +If freeze_params is called without arguments, all input parameters will be frozen. +Otherwise, passing a set of parameters will inform the decorator of which named +parameters must be frozen. + +```python +from typing import List +from gelidum import freeze_params + +@freeze_params(params={"list1", "list2"}) +def concat_lists(dest: List, list1: List, list2: List) -> List: + dest = list1 + list2 + return dest + +# Freeze dest, list1 and list2 +concat_lists([], list1=[1, 2, 3], list2=[4, 5, 6]) + +# Freeze list1 and list2 +concat_lists(dest=[], list1=[1, 2, 3], list2=[4, 5, 6]) +``` + +Always use kwargs unless you want to freeze the args params. A good way to enforce this is by making the +function have keyword-only arguments: + +```python +from typing import List +from gelidum import freeze_params + +@freeze_params(params={"list1", "list2"}) +def concat_lists_in(*, dest: List, list1: List, list2: List): + dest = list1 + list2 + return dest +``` + +You can use the **Final typehint from gelidum** to signal that an argument is immutable: + +```python +from typing import List +from gelidum import freeze_final, Final + +@freeze_final +def concatenate_lists(list1: Final[List], list2: Final[List]): + return list1 + list2 +``` + +Finally, take in account that all freezing is done in a new object (i.e. freeze with on_freeze="copy"). +It makes no sense to freeze a parameter of a function that could be used later, *outside* +said function. + +### Check original (i.e. "hot") class +- **get_gelidum_hot_class_name**: returns the name of hot class. +- **get_gelidum_hot_class_module** returns the module reference where the hot class was. + +## Collections +There are four immutable collections in the gelidum.collections module. + +- frozendict +- frozenlist +- frozenzet (frozenset is already a builtin type in Python) + +All of these classes can be used to make sure a collection of objects +is not modified. Indeed, when creating a new collection object, you +can pass a custom freeze function, to customize the freezing process +of each of its items, e.g.: + +```python +import logging +from gelidum.freeze import freeze +from gelidum.collections import frozenzet +from gelidum.typing import FrozenType +from typing import Any + + +def my_freeze_func(item: Any) -> FrozenType: + logging.debug(f"Freezing item {item}") + return freeze(item, on_update="exception", on_freeze="copy") + +frozen_zet = frozenzet([1, 2, 3], freeze_func=my_freeze_func) +``` + +## Rationale and background information +Inspired by my old work with Ruby on Rails, I decided to create a mechanism to make +objects immutable in Python. The first aim was to do a tool to avoid accidental +modifications on the objects while passing them through an execution flow. + +Anyways, as time passed I thought that an implementation of a programming language +with real threading support (i.e. not cpython) could be benefited from this feature. +I know that both cpython and pypy implementations of the Python programming +language have a [GIL](https://en.wikipedia.org/wiki/Global_interpreter_lock) but IronPython +and [Graalpython](https://github.com/oracle/graalpython) don't. +IronPython3 has no support for typehintings yet, +but Graalpython seems to work fine, so more experiments will be coming. + +On the other hand, I'm also interested in creating functional data structures +in this package, easing the life of developers that do not want side effects. + +It's true that the complexity of Python does not play well with this kind of library. +Thus, Python usually serves as easy interface with native libraries (pandas, numpy, etc.) +However, this project is fun to develop and maybe with the popularity of alternative +implementations of Python some work can be done to improve performance. + +More information can be seen in this [Show HN post](https://news.ycombinator.com/item?id=27507524) +and some appreciated feedback of the users of that great community. + +## Limitations +- dict, list, tuple and set objects cannot be modified inplace although the flag inplace is set. +- file handler attributes are not supported. An exception is raised when trying to freeze + an object with them. +- frozen objects cannot be serialized with [marshal](https://docs.python.org/3/library/marshal.html). +- frozen objects cannot be (deep)-copied. This limitation is intended to make structural sharing easier. +- Classes with \_\_slots\_\_: + - cannot be frozen in-place. + - will be frozen with a unique class. The frozen class will not be shared by instances of the same class. + +## Advice & comments on use +### On_update parameter of freeze function +Use on_update with a callable to store when somebody tried to write in the immutable object: +```python +import datetime +import logging +import threading +from gelidum import freeze + + +class Dummy(object): + def __init__(self, attr: int): + self.attr = attr + + +class FrozenDummyUpdateTryRecorder: + LOCK = threading.Lock() + written_tries = [] + + @classmethod + def add_writing_try(cls, message, *args, **kwargs): + logging.warning(message) + with cls.LOCK: + cls.written_tries.append({ + "message": message, + "args": args, + "kwargs": kwargs, + "datetime": datetime.datetime.utcnow() + }) + + +dummy = Dummy(1) +frozen_dummy = freeze( + dummy, + on_update=FrozenDummyUpdateTryRecorder.add_writing_try + ) +# It will call FrozenDummyUpdateTryRecorder.add_writing_try +# and will continue the execution flow with the next sentence. +frozen_dummy.attr = 4 +``` + +### On_freeze parameter of freeze function +The parameter on_freeze of the function freeze must be a string or a callable. +This parameter informs of what to do with the object that will be frozen. +Should it be the same input object frozen or a copy of it? + +If it has a string as parameter, values "inplace" and "copy" are allowed. +A value of "inplace" will make the freeze method to try to freeze the object +as-is, while a value of "copy" will make a copy of the original object and then, +freeze that copy. **These are the recommended parameters**. + +On the other hand, the interesting part is to define a custom on_freeze method. +This method must return an object of the same type of the input. +**This returned will be frozen, and returned to the caller of freeze**. + +Note this parameter has no interference with the structural sharing of the frozen objects. +Any frozen object that have several references to it will be shared, not copied. + +```python +import copy + +def on_freeze(self, obj: object) -> object: + frozen_object = copy.deepcopy(obj) + # log, copy the original method or do any other + # custom action in this function + return frozen_object +``` +As seen earlier, there is also the possibility to +pass a callable object. If you would like you can even +define your own on_freeze functions by inheriting +from classes: + +- OnFreezeCopier +- OnFreezeIdentityFunc + +See some examples in [on_freeze.py](/gelidum/on_freeze.py) file. + +## Dependencies +This package has no dependencies. + + +## Roadmap +- [x] Freeze only when attributes are modified? + Not exactly but structural sharing is used. +- [x] Include immutable collections. +- [ ] [Graalpython](https://github.com/oracle/graalpython) support. +- [ ] Make some use-cases with threading/async module (i.e. server) + + +## Collaborations +This project is open to collaborations. Make a PR or an issue, +and I'll take a look to it. + +## License +[MIT](LICENSE) license, but if you need any other contact me. + + +%prep +%autosetup -n gelidum-0.6.0 + +%build +%py3_build + +%install +%py3_install +install -d -m755 %{buildroot}/%{_pkgdocdir} +if [ -d doc ]; then cp -arf doc %{buildroot}/%{_pkgdocdir}; fi +if [ -d docs ]; then cp -arf docs %{buildroot}/%{_pkgdocdir}; fi +if [ -d example ]; then cp -arf example %{buildroot}/%{_pkgdocdir}; fi +if [ -d examples ]; then cp -arf examples %{buildroot}/%{_pkgdocdir}; fi +pushd %{buildroot} +if [ -d usr/lib ]; then + find usr/lib -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/lib64 ]; then + find usr/lib64 -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/bin ]; then + find usr/bin -type f -printf "/%h/%f\n" >> filelist.lst +fi +if [ -d usr/sbin ]; then + find usr/sbin -type f -printf "/%h/%f\n" >> filelist.lst +fi +touch doclist.lst +if [ -d usr/share/man ]; then + find usr/share/man -type f -printf "/%h/%f.gz\n" >> doclist.lst +fi +popd +mv %{buildroot}/filelist.lst . +mv %{buildroot}/doclist.lst . + +%files -n python3-gelidum -f filelist.lst +%dir %{python3_sitelib}/* + +%files help -f doclist.lst +%{_docdir}/* + +%changelog +* Fri May 05 2023 Python_Bot <Python_Bot@openeuler.org> - 0.6.0-1 +- Package Spec generated @@ -0,0 +1 @@ +2206f282ef898bfae8ffb52b658d847d gelidum-0.6.0.tar.gz |