Source code for pydeco.decorator

"""Decorator for class methods (compatible with Python 3.0 or higher).

The herebelow class :class:`MethodsDecorator` enables to decorate multiple
methods of a given class with custom decorators by simply decorating said class
with it. One then needs to specify, as a dictionary, which decorator is used to
decorate which methods.

"""
import logging
import re
from abc import abstractmethod
from copy import deepcopy
from functools import wraps

from .utils import CONFIG, is_wrapped

global shared_wrappers
shared_wrappers = globals()

from .utils.register import (assign, dispatch,  # noqa: E402
                             get_first_unassigned_wrapper,
                             make_wrapper_classname, register)


[docs]class Decorator(object): """Decorator base class.""" def __init__(self, *args, **kwargs): self.instances = []
[docs] def flush_instances(self): """Flush instances.""" self.instances = []
[docs] @abstractmethod def wrapper(self, instance, func, *args, **kwargs): """Wrap func.""" pass
[docs] def activate(self): """Activate decorator for all methods of decorated instances.""" for instance in self.instances: instance.activate_decorator(self.__class__.__name__)
[docs] def deactivate(self): """Deactivate decorator for all methods of decorated instances.""" for instance in self.instances: instance.deactivate_decorator(self.__class__.__name__)
[docs] def is_active(self, instance): """Return True if decorator is active for input instance.""" if instance not in self.instances: err = ('Current decorator does not decorate a method of input ' 'class instance.') raise ValueError(err) if not is_wrapped(instance): return True else: class_name = self.__class__.__name__ # decorator not used in :class:`MethodsDecorator` context if class_name not in instance.decorators: return True else: return instance.is_decorator_active(class_name)
def __call__(self, func): """Call.""" def wrapped_func(instance, func, *args, **kwargs): """Call wrapper is decorator is active, otherwise call func.""" if instance not in self.instances: self.instances.append(instance) if self.is_active(instance): # active decorator for the current func: wrap it # check if input is a bound method or not if hasattr(func, '__self__'): # wrap func as it is a bound method def func_(instance, *args, **kwargs): return func(*args, **kwargs) return self.wrapper(instance, func_, *args, **kwargs) else: return self.wrapper(instance, func, *args, **kwargs) else: # inactive decorator for the current func return func(instance, *args, **kwargs) if hasattr(func, '__self__'): # input object is a method of an instance instance = func.__self__ @wraps(func) def _wrapped_func(*args, **kwargs): return wrapped_func(instance, func, *args, **kwargs) return _wrapped_func else: @wraps(func) def _wrapped_func(instance, *args, **kwargs): return wrapped_func(instance, func, *args, **kwargs) return _wrapped_func
[docs]class MethodsDecorator(object): """Class that enables to decorate specific methods with given decorator. Parameters ---------- mapping : dict Mapping containing decorators as key and methods (as str or a list of str) as values (ex: ``{Timer(): ['fit', 'predict']}``) Examples -------- We assume here that a decorator named :class:`Timer` (used to compute running time of called functions) is already implemented. The below code applies such a decorator to the method :meth:`method_1` of class :class:`MyClass` so as to compute running time every time :meth:`method_1` is called. >>> @MethodsDecorator(mapping={Timer(): 'method_1'}) >>> class MyClass(): >>> >>> def method_1(self, *args, **kwargs): >>> return The "@" syntax is a proxy for the below implementation: >>> class MyClass(): >>> >>> def method_1(self, *args, **kwargs): >>> return >>> >>> MyClass = MethodsDecorator(mapping={Timer(): 'method_1'})(MyClass) See the examples section for more insights on how to use :class:`MethodsDecorator`. """ def __init__(self, mapping={}): self.mapping = mapping for decorator, methods in mapping.items(): if not isinstance(methods, (tuple, list)): methods = [methods] assert callable(decorator) assert all([isinstance(method, str) for method in methods]) self.mapping[decorator] = methods self.original_methods = dict() def __call__(self, cls): """Return wrapped input class with decorated methods.""" mapping = self.mapping original_methods = self.original_methods class MC(type): """Decorating methods for the input class with given decorator. Attributes ---------- decorated : bool Indicates that current class is decorated. class_decorator : type Type of the class decorator used to decorate the current class. """ def __init__(cls_, name, bases, dict): for decorator, methods in self.mapping.items(): for method in methods: if not hasattr(cls_, method): err = 'Input class has not method "{}"'.format( method) raise ValueError(err) if method not in self.original_methods: self.original_methods[method] = ( getattr(cls_, method) ) setattr(cls_, method, decorator(getattr(cls_, method))) super(MC, cls_).__init__(name, bases, dict) global Wrapper class Wrapper(cls, metaclass=MC): """Wrapped class where each specified method is decorated.""" __wrapped_class = cls # base class __decorated = True # indicates that the class is decorated __wrapper = self.__class__ # type of class decorator __decorator_mapping = mapping # decorator mapping __original_methods = original_methods __assigned = False def __init__(self, *args, **kwargs): self._decorator_mapping = { k: v for k, v in self.__decorator_mapping.items() } self.active_decorators = { decorator: True for decorator in self.decorators } self.__class__.__assigned = True cls.__init__(self, *args, **kwargs) @property def decorators(self): """Return decorators.""" return { decorator.__class__.__name__: decorator for decorator in self._decorator_mapping.keys() } def __deepcopy__(self, memo=None, _nil=[]): """Deepcopy.""" # Remove decorators from self cls_self = self.__class__ tmp_methods = dict() tmp_mapping = dict() tmp_active_decorators = dict() for decorator, methods in self._decorator_mapping.items(): tmp_mapping[decorator] = methods for decorator, is_active in self.active_decorators.items(): tmp_active_decorators[decorator] = is_active self._decorator_mapping = dict() self.active_decorators = dict() for method_name, method in self.__original_methods.items(): tmp_methods[method_name] = method delattr(cls_self, method_name) # Copy self c_self = cls_self.__new__(cls_self) memo[id(self)] = c_self for k, v in self.__dict__.items(): setattr(c_self, k, deepcopy(v, memo)) # Copy self's Wrapper cls_c_self = get_first_unassigned_wrapper(cls.__name__) assign(cls_c_self) c_self.__class__ = cls_c_self # Add back decorators to both self and its copy for method_name, method in tmp_methods.items(): setattr(cls_self, method_name, method) setattr(cls_c_self, method_name, method) for decorator, methods in tmp_mapping.items(): decorator_name = decorator.__class__.__name__ c_decorator = deepcopy(decorator) self._decorator_mapping[decorator] = methods c_self._decorator_mapping[c_decorator] = methods is_decorator_active = ( tmp_active_decorators[decorator_name] ) self.active_decorators[decorator_name] = ( is_decorator_active) c_self.active_decorators[decorator_name] = ( is_decorator_active) for method_name in methods: if not hasattr(self, method_name): err = 'Input class has not method "{}"'.format( method_name) raise ValueError(err) setattr( cls_self, method_name, decorator(getattr(cls_self, method_name))) if not hasattr(c_self, method_name): err = 'Input class has not method "{}"'.format( method_name) raise ValueError(err) setattr( cls_c_self, method_name, c_decorator(getattr(cls_c_self, method_name))) # return copy return c_self def _check_decorator_name(self, name): if name not in self.decorators: err = ('Could not find decorator "{}". Available ' 'decorators: {}'.format( name, list(self.decorators.keys()))) raise ValueError(err) def is_decorator_active(self, name): """Check if input decorator is active.""" self._check_decorator_name(name) return self.active_decorators[name] def activate_decorator(self, name): """Activate decorator.""" self._check_decorator_name(name) self.active_decorators[name] = True def deactivate_decorator(self, name): """Deactivate decorator.""" self._check_decorator_name(name) self.active_decorators[name] = False # Updating wrapped class name and documentation Wrapper.__name__ = make_wrapper_classname(cls.__name__) Wrapper.__doc__ = cls.__doc__ # Registering newly created Wrapper class register(Wrapper) # Dispatch Wrapper for multiprocessing purpose dispatch(Wrapper, n_dispatch=CONFIG['N_DISPATCH']) return Wrapper