├── .gitignore ├── LICENSE ├── README.md ├── requirements.txt ├── setup.py └── src └── rememberer ├── __init__.py ├── rememberer.py └── test ├── test_load_obj.py ├── test_rem.py └── test_save_obj.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | 132 | # PyCharm project files 133 | .idea 134 | .pypirc 135 | 136 | # this is genreated by test runner 137 | src/_trial_temp 138 | src/rememberer/test/_trial_temp 139 | *.pkl 140 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ChamRun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rememberer 2 | Rememberer is a tool to help your functions remember their previous results. 3 | 4 | 5 | The advantage of this package compared to other memoization packages is that 6 | it will remember the result of the function even if you kill the program and restart it. 7 | 8 | It will also remember the result even if you restart the python interpreter because 9 | it uses a pickle file to store the results. 10 | 11 | ## Installation 12 | 13 | ```bash 14 | pip install rememberer 15 | ``` 16 | 17 | ## Usage 18 | 19 | ```python 20 | from rememberer import rem 21 | 22 | def add(a, b): 23 | import time 24 | time.sleep(3) 25 | return a + b 26 | 27 | rem(add, 1, b=2) # this will take 3 seconds 28 | rem(add, 1, b=2) # this will take ~0 seconds 29 | ``` 30 | 31 | You can use it as a decorator as well: 32 | 33 | ```python 34 | from rememberer import rem_dec 35 | 36 | @rem_dec 37 | def add(a, b): 38 | import time 39 | time.sleep(3) 40 | return a + b 41 | 42 | add(1, b=2) # this will take 3 seconds 43 | add(1, b=2) # this will take ~0 seconds 44 | ``` 45 | 46 | 47 | If you want to clear the cache, you can use the `forget` method: 48 | 49 | ```python 50 | from rememberer import forget 51 | 52 | forget(add, 1, b=2) 53 | 54 | ``` 55 | 56 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | setuptools==45.2.0 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import pathlib 3 | 4 | here = pathlib.Path(__file__).parent.resolve() 5 | 6 | long_description = (here / "README.md").read_text(encoding="utf-8") 7 | 8 | setup( 9 | name="rememberer", 10 | version="0.1.5", 11 | license="MIT", 12 | description="Rememberer is a tool to help your functions remember their previous results.", 13 | long_description=long_description, 14 | long_description_content_type="text/markdown", 15 | url="https://github.com/ChamRun/rememberer", 16 | author="ChamRun", 17 | author_email="chrm@aut.ac.ir", 18 | keywords="pickle, cache", 19 | packages=find_packages('src'), 20 | package_dir={"": "src"}, 21 | python_requires=">=3.5", 22 | install_requires=["pickle5"], 23 | project_urls={ 24 | "Organization": "https://chamrun.github.io/", 25 | }, 26 | ) 27 | -------------------------------------------------------------------------------- /src/rememberer/__init__.py: -------------------------------------------------------------------------------- 1 | from .rememberer import * 2 | -------------------------------------------------------------------------------- /src/rememberer/rememberer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | import hashlib 4 | from functools import wraps 5 | from typing import AnyStr 6 | from types import FunctionType 7 | 8 | 9 | def save_obj(obj: object, name: str = None, path: str = './rem/') -> AnyStr: 10 | """ 11 | Serialize and save the given object to disk. 12 | 13 | Parameters: 14 | obj (object): The object to be serialized and saved. 15 | name (str): The name of the file to be saved. If not given, a SHA256 hash of the object will be used. 16 | path (str): The path to the directory where the file will be saved. Default is './rem/'. 17 | 18 | Returns: 19 | AnyStr: The absolute path of the saved file. 20 | """ 21 | if path[-1] != '/': 22 | path += '/' 23 | 24 | if not name: 25 | hash_object = hashlib.sha256() 26 | hash_object.update(pickle.dumps(obj)) 27 | name = hash_object.hexdigest() 28 | 29 | current_dir = os.getcwd() 30 | for folder_name in path.split('/'): 31 | if not folder_name: 32 | continue 33 | 34 | if not os.path.exists(folder_name): 35 | os.mkdir(folder_name) 36 | 37 | os.chdir(folder_name) 38 | 39 | with open(f'{name}.pkl', 'wb') as f: 40 | pickle.dump(obj, f, pickle.HIGHEST_PROTOCOL) 41 | abspath = os.path.abspath(f.name) 42 | 43 | os.chdir(current_dir) 44 | return abspath 45 | 46 | 47 | def load_obj(name: str, path: str = './rem/'): 48 | """ 49 | Load and deserialize the object saved at the given path. 50 | 51 | Parameters: 52 | name (str): The name of the file to be loaded. 53 | path (str): The path to the directory where the file is saved. Default is './rem/'. 54 | 55 | Returns: 56 | object: The deserialized object. 57 | """ 58 | if path[-1] != '/': 59 | path += '/' 60 | 61 | if not (name.endswith('.pkl') or name.endswith('.pickle')): 62 | name += '.pkl' 63 | 64 | try: 65 | with open(f'{path}{name}', 'rb') as f: 66 | return pickle.load(f) 67 | except FileNotFoundError: 68 | return None 69 | 70 | 71 | def rem(func: FunctionType, *args, **kwargs) -> object: 72 | """ 73 | This is a function that can be applied to another function, it will cache the result of the function 74 | based on the arguments passed to it, so that if the same arguments are passed again, the cached result will be 75 | returned instead of re-computing the result. 76 | 77 | Parameters: 78 | func (FunctionType): The function that this decorator will be applied to. 79 | *args: Positional arguments that will be passed to the function. 80 | **kwargs: Keyword arguments that will be passed to the function. 81 | 82 | Returns: 83 | The result of the function call. 84 | """ 85 | 86 | name = _create_name(func, args, kwargs) 87 | saved = load_obj(name) 88 | if saved is not None: 89 | return saved 90 | 91 | result = func(*args, **kwargs) 92 | save_obj(result, name) 93 | return result 94 | 95 | 96 | def forget(func: FunctionType, *args, **kwargs): 97 | """ 98 | This is a function that can be applied to another function, it will delete the cached result of the function 99 | based on the arguments passed to it. 100 | 101 | Parameters: 102 | func (FunctionType): The function that this decorator will be applied to. 103 | *args: Positional arguments that will be passed to the function. 104 | **kwargs: Keyword arguments that will be passed to the function. 105 | 106 | Returns: 107 | The result of the function call. 108 | """ 109 | name = _create_name(func, args, kwargs) 110 | try: 111 | os.remove(f'./rem/{name}.pkl') 112 | except FileNotFoundError: 113 | pass 114 | 115 | 116 | def _create_name(func: FunctionType, args: tuple, kwargs: dict) -> str: 117 | """ 118 | Create a name for the cached result of the function based on the arguments passed to it. 119 | 120 | Parameters: 121 | func (function): The function that this decorator will be applied to. 122 | *args: Positional arguments that will be passed to the function. 123 | **kwargs: Keyword arguments that will be passed to the function. 124 | 125 | Returns: 126 | The name of the cached result. 127 | """ 128 | 129 | def stringify(obj: object) -> str: 130 | """ 131 | Convert the given object to a string. 132 | 133 | Parameters: 134 | obj (object): The object to be converted to a string. 135 | 136 | Returns: 137 | The string representation of the given object. 138 | 139 | Examples: 140 | >>> stringify(123.456) 141 | '123.456' 142 | 143 | >>> stringify(True) 144 | 'True' 145 | 146 | >>> stringify({'a': 1, 'b': 2}) 147 | "{'a': 1, 'b': 2}" 148 | 149 | >>> stringify({1, 2, 3}) 150 | '{1, 2, 3}' 151 | 152 | >>> stringify(print) 153 | '' 154 | 155 | >>> class A: 156 | ... def __init__(self, a, b): 157 | ... self.a = a 158 | ... self.b = b 159 | ... 160 | ... def __repr__(self): 161 | ... return f'A({self.a}, {self.b})' 162 | >>> stringify(A(1, 2)) 163 | 'A(1, 2)' 164 | """ 165 | return str(obj) if isinstance(obj, (int, float, str, bool)) else repr(obj) 166 | 167 | params = (func.__module__ + func.__name__).encode() + func.__code__.co_code + b"".join( 168 | stringify(arg).encode() for arg in args) + b"".join( 169 | f"{key}={stringify(value)}".encode() for key, value in kwargs.items()) 170 | name = hashlib.sha256(params).hexdigest() 171 | return name 172 | 173 | 174 | def rem_dec(func: FunctionType) -> FunctionType: 175 | """ 176 | This is a decorator that can be applied to another function, it will cache the result of the function 177 | based on the arguments passed to it, so that if the same arguments are passed again, the cached result will be 178 | returned instead of re-computing the result. 179 | 180 | Parameters: 181 | func (FunctionType): The function that this decorator will be applied to. 182 | 183 | Returns: 184 | The result of the function call. 185 | """ 186 | 187 | @wraps(func) # This is for the sake of the documentation 188 | def wrapper(*args, **kwargs) -> object: 189 | result = rem(func, *args, **kwargs) 190 | return result 191 | 192 | return wrapper 193 | -------------------------------------------------------------------------------- /src/rememberer/test/test_load_obj.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from rememberer import save_obj, load_obj 3 | 4 | 5 | class TestLoadObj(unittest.TestCase): 6 | def test_load_obj(self): 7 | obj = {'name': 'John', 'age': 30} 8 | name = 'test_obj' 9 | path = './test_rem/' 10 | save_obj(obj, name, path) 11 | loaded_obj = load_obj(name, path) 12 | self.assertEqual(obj, loaded_obj) 13 | 14 | def test_load_obj_with_default_path(self): 15 | obj = {'name': 'John', 'age': 30} 16 | name = 'test_obj' 17 | save_obj(obj, name) 18 | loaded_obj = load_obj(name) 19 | self.assertEqual(obj, loaded_obj) 20 | 21 | def test_load_obj_with_non_existent_file(self): 22 | name = 'non_existent_file' 23 | loaded_obj = load_obj(name) 24 | self.assertIsNone(loaded_obj) 25 | 26 | 27 | if __name__ == '__main__': 28 | unittest.main() 29 | -------------------------------------------------------------------------------- /src/rememberer/test/test_rem.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import time 3 | 4 | from rememberer import rem 5 | 6 | 7 | def test_func(a, b): 8 | time.sleep(2) 9 | return a + b 10 | 11 | 12 | class TestRem(unittest.TestCase): 13 | def test_rem(self): 14 | result1 = rem(test_func, 1, 2) 15 | 16 | start = time.time() 17 | result2 = rem(test_func, 1, 2) 18 | end = time.time() 19 | 20 | self.assertLess(end - start, 1) 21 | self.assertEqual(result1, result2) 22 | 23 | def test_rem_with_kwargs(self): 24 | result = rem(test_func, 1, b=2) 25 | self.assertEqual(result, 3) 26 | 27 | def test_rem_with_cached_result(self): 28 | rem(test_func, 1, 2) 29 | result = rem(test_func, 1, 2) 30 | self.assertEqual(result, 3) 31 | 32 | def test_methods(self): 33 | class Test: 34 | def test_method(self, a, b): 35 | time.sleep(2) 36 | return a + b 37 | 38 | test = Test() 39 | result1 = rem(test.test_method, 1, 2) 40 | 41 | start = time.time() 42 | result2 = rem(test.test_method, 1, 2) 43 | end = time.time() 44 | 45 | self.assertLess(end - start, 1) 46 | self.assertEqual(result1, result2) 47 | 48 | def test_methods_with_kwargs(self): 49 | class Test: 50 | def test_method(self, a, b): 51 | time.sleep(2) 52 | return a + b 53 | 54 | test = Test() 55 | result1 = rem(test.test_method, 1, b=2) 56 | 57 | start = time.time() 58 | result2 = rem(test.test_method, 1, b=2) 59 | end = time.time() 60 | 61 | self.assertEqual(result2, 3) 62 | self.assertEqual(result2, result1) 63 | self.assertLess(end - start, 1) 64 | 65 | def test_dicts(self): 66 | def test_func2(a, b): 67 | time.sleep(2) 68 | return a['a'] + b['b'] 69 | 70 | result1 = rem(test_func2, {'a': 1}, {'b': 2}) 71 | 72 | start = time.time() 73 | result2 = rem(test_func2, {'a': 1}, {'b': 2}) 74 | end = time.time() 75 | 76 | self.assertLess(end - start, 1) 77 | self.assertEqual(result1, result2) 78 | 79 | 80 | if __name__ == '__main__': 81 | unittest.main() 82 | -------------------------------------------------------------------------------- /src/rememberer/test/test_save_obj.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | 4 | from rememberer import save_obj 5 | 6 | 7 | class TestSaveObj(unittest.TestCase): 8 | def test_save_obj(self): 9 | obj = {'name': 'John', 'age': 30} 10 | name = 'test_obj' 11 | path = './test_rem/' 12 | save_obj(obj, name, path) 13 | self.assertTrue(os.path.exists(path + name + '.pkl')) 14 | 15 | def test_save_obj_with_default_name(self): 16 | obj = {'name': 'John', 'age': 30} 17 | path = './test_rem/' 18 | save_path = save_obj(obj, path=path) 19 | self.assertTrue(os.path.exists(save_path)) 20 | 21 | def test_save_obj_with_default_path(self): 22 | obj = {'name': 'John', 'age': 30} 23 | name = 'test_obj' 24 | save_path = save_obj(obj, name) 25 | self.assertTrue(os.path.exists('./rem/' + name + '.pkl')) 26 | 27 | 28 | if __name__ == '__main__': 29 | unittest.main() 30 | --------------------------------------------------------------------------------