summaryrefslogtreecommitdiff
path: root/passlib/utils/decor.py
blob: aa40925b54329605847510765a06f391348accae (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
"""
passlib.utils.decor -- helper decorators & properties
"""
#=============================================================================
# imports
#=============================================================================
# core
import logging
log = logging.getLogger(__name__)
from functools import wraps, update_wrapper
import types
from warnings import warn
# site
# pkg
# 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
        return types.MethodType(self.func, obj)

#=============================================================================
# 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

    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:
            func = func.__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
#=============================================================================