""" passlib.utils.decor -- helper decorators & properties """ #============================================================================= # imports #============================================================================= # core from __future__ import absolute_import, division, print_function import logging log = logging.getLogger(__name__) from functools import wraps, update_wrapper import types from warnings import warn # site # pkg from passlib.utils.compat import PY3 # local __all__ = [ "classproperty", "hybrid_method", "memoize_single_value", "memoized_property", "deprecated_function", "deprecated_method", ] #============================================================================= # class-level decorators #============================================================================= class classproperty(object): """Function decorator which acts like a combination of classmethod+property (limited to read-only properties)""" def __init__(self, func): self.im_func = func def __get__(self, obj, cls): return self.im_func(cls) @property def __func__(self): """py3 compatible alias""" return self.im_func class hybrid_method(object): """ decorator which invokes function with class if called as class method, and with object if called at instance level. """ def __init__(self, func): self.func = func update_wrapper(self, func) def __get__(self, obj, cls): if obj is None: obj = cls if PY3: return types.MethodType(self.func, obj) else: return types.MethodType(self.func, obj, cls) #============================================================================= # memoization #============================================================================= def memoize_single_value(func): """ decorator for function which takes no args, and memoizes result. exposes a ``.clear_cache`` method to clear the cached value. """ cache = {} @wraps(func) def wrapper(): try: return cache[True] except KeyError: pass value = cache[True] = func() return value def clear_cache(): cache.pop(True, None) wrapper.clear_cache = clear_cache return wrapper class memoized_property(object): """ decorator which invokes method once, then replaces attr with result """ def __init__(self, func): self.__func__ = func self.__name__ = func.__name__ self.__doc__ = func.__doc__ def __get__(self, obj, cls): if obj is None: return self value = self.__func__(obj) setattr(obj, self.__name__, value) return value if not PY3: @property def im_func(self): """py2 alias""" return self.__func__ def clear_cache(self, obj): """ class-level helper to clear stored value (if any). usage: :samp:`type(self).{attr}.clear_cache(self)` """ obj.__dict__.pop(self.__name__, None) def peek_cache(self, obj, default=None): """ class-level helper to peek at stored value usage: :samp:`value = type(self).{attr}.clear_cache(self)` """ return obj.__dict__.get(self.__name__, default) # works but not used ##class memoized_class_property(object): ## """function decorator which calls function as classmethod, ## and replaces itself with result for current and all future invocations. ## """ ## def __init__(self, func): ## self.im_func = func ## ## def __get__(self, obj, cls): ## func = self.im_func ## value = func(cls) ## setattr(cls, func.__name__, value) ## return value ## ## @property ## def __func__(self): ## "py3 compatible alias" #============================================================================= # deprecation #============================================================================= def deprecated_function(msg=None, deprecated=None, removed=None, updoc=True, replacement=None, _is_method=False, func_module=None): """decorator to deprecate a function. :arg msg: optional msg, default chosen if omitted :kwd deprecated: version when function was first deprecated :kwd removed: version when function will be removed :kwd replacement: alternate name / instructions for replacing this function. :kwd updoc: add notice to docstring (default ``True``) """ if msg is None: if _is_method: msg = "the method %(mod)s.%(klass)s.%(name)s() is deprecated" else: msg = "the function %(mod)s.%(name)s() is deprecated" if deprecated: msg += " as of Passlib %(deprecated)s" if removed: msg += ", and will be removed in Passlib %(removed)s" if replacement: msg += ", use %s instead" % replacement msg += "." def build(func): is_classmethod = _is_method and isinstance(func, classmethod) if is_classmethod: # NOTE: PY26 doesn't support "classmethod().__func__" directly... func = func.__get__(None, type).__func__ opts = dict( mod=func_module or func.__module__, name=func.__name__, deprecated=deprecated, removed=removed, ) if _is_method: def wrapper(*args, **kwds): tmp = opts.copy() klass = args[0] if is_classmethod else args[0].__class__ tmp.update(klass=klass.__name__, mod=klass.__module__) warn(msg % tmp, DeprecationWarning, stacklevel=2) return func(*args, **kwds) else: text = msg % opts def wrapper(*args, **kwds): warn(text, DeprecationWarning, stacklevel=2) return func(*args, **kwds) update_wrapper(wrapper, func) if updoc and (deprecated or removed) and \ wrapper.__doc__ and ".. deprecated::" not in wrapper.__doc__: txt = deprecated or '' if removed or replacement: txt += "\n " if removed: txt += "and will be removed in version %s" % (removed,) if replacement: if removed: txt += ", " txt += "use %s instead" % replacement txt += "." if not wrapper.__doc__.strip(" ").endswith("\n"): wrapper.__doc__ += "\n" wrapper.__doc__ += "\n.. deprecated:: %s\n" % (txt,) if is_classmethod: wrapper = classmethod(wrapper) return wrapper return build def deprecated_method(msg=None, deprecated=None, removed=None, updoc=True, replacement=None): """decorator to deprecate a method. :arg msg: optional msg, default chosen if omitted :kwd deprecated: version when method was first deprecated :kwd removed: version when method will be removed :kwd replacement: alternate name / instructions for replacing this method. :kwd updoc: add notice to docstring (default ``True``) """ return deprecated_function(msg, deprecated, removed, updoc, replacement, _is_method=True) #============================================================================= # eof #=============================================================================