Source code for mutwo.core_utilities.decorators

"""Generic decorators that are used within :mod:`mutwo`."""

import copy
import functools
import inspect
import os
import types
import typing

__all__ = ("add_copy_option", "add_tag_to_class", "compute_lazy")

from mutwo import core_utilities


F = typing.TypeVar("F", bound=typing.Callable[..., typing.Any])


[docs]def add_copy_option(function: F) -> F: """This decorator adds a copy option for object mutating methods. :arg function: The method which shall be adjusted. The 'add_copy_option' decorator adds the 'mutate' keyword argument to the decorated method. If 'mutate' is set to ``False``, the decorator deep copies the respective object, then applies the called method on the new copied object and finally returns the copied object. This can be useful for methods that by default mutate its object. When adding this method, it is up to the user whether the original object shall be changed and returned (for mutate=True) or if a copied version of the object with the respective mutation shall be returned (for mutate=False). """ @functools.wraps(function) def wrapper(self, *args, mutate: bool = True, **kwargs) -> typing.Any: if mutate is True: function(self, *args, **kwargs) return self else: deep_copied_object = copy.deepcopy(self) function(deep_copied_object, *args, **kwargs) return deep_copied_object wrapped_function = typing.cast(F, wrapper) wrapped_function.__annotations__.update({"mutate": bool}) return wrapped_function
G = typing.TypeVar("G")
[docs]def add_tag_to_class(class_to_decorate: G) -> G: """This decorator adds a 'tag' argument to the init method of a class. :arg class_to_decorate: The class which shall be decorated. """ init = class_to_decorate.__init__ def init_with_tag(self, *args, tag: typing.Optional[str] = None, **kwargs): init(self, *args, **kwargs) self.tag = tag class_to_decorate.__init__ = init_with_tag return class_to_decorate
[docs]def compute_lazy( path: str, force_to_compute: bool = False, pickle_module: typing.Optional[types.ModuleType] = None, ): """Cache function output to disk via pickle. :param path: Where to save the computed result. :type path: str :param force_to_compute: Set to ``True`` if function has to be re-computed. :type force_to_compute: bool :param pickle_module: Depending on the object which should be pickled the default python pickle module won't be sufficient. Therefore alternative third party pickle modules (with the same API) can be used. If no argument is provided, the function will first try to use any of the pickle modules given in the :const:`mutwo.core_utilities.configurations.PICKLE_MODULE_TO_SEARCH_TUPLE`. If none of the modules could be imported it will fall back to the buildin pickle module. :type pickle_module: typing.Optional[types.ModuleType] The decorator will only run the function if its input changes and otherwise load the return value from the disk. This function is helpful if there is a complex, long-taking calculation, which should only run once or from time to time if the input changes. **Example:** >>> from mutwo.utilities import decorators >>> @decorators.compute_lazy("magic_output", False) def my_super_complex_calculation(n_numbers): return sum(number for number in range(n_numbers)) >>> N_NUMBERS = 100000000 >>> my_super_complex_calculation(N_NUMBERS) 4999999950000000 >>> # takes very little time when calling the function the second time >>> my_super_complex_calculation(N_NUMBERS) 4999999950000000 >>> # takes long again, because the input changed >>> my_super_complex_calculation(N_NUMBERS + 10) 4999999950000000 """ if pickle_module is None: for ( pickle_module_name ) in core_utilities.configurations.PICKLE_MODULE_TO_SEARCH_TUPLE: try: pickle_module = __import__(pickle_module_name) except ImportError: pass else: break if pickle_module is None: pickle_module = __import__("pickle") def decorator(function_to_decorate: F) -> F: @functools.wraps(function_to_decorate) def wrapper(*args, **kwargs) -> typing.Any: has_to_compute = False current_args_and_kwargs = (args, kwargs) is_file = os.path.isfile(path) if not is_file: has_to_compute = True else: with open(path, "rb") as f: function_result, previous_args_and_kwargs = pickle_module.load(f) if previous_args_and_kwargs != current_args_and_kwargs: has_to_compute = True if has_to_compute or force_to_compute: function_result = function_to_decorate(*args, **kwargs) with open(path, "wb") as f: pickle_module.dump((function_result, current_args_and_kwargs), f) return function_result wrapped_function = typing.cast(F, wrapper) return wrapped_function return decorator