1
2"""Decorators and helper functions for writing well behaved decorators."""
3
4from inspect import getargspec, formatargspec
5from threading import RLock
6
7from application.python.weakref import weakobjectmap
8
9
10__all__ = 'decorator', 'preserve_signature', 'execute_once'
11
12
13def decorator(func):
14    """A syntactic marker with no other effect than improving readability."""
15    return func
16
17
18def preserve_signature(func):
19    """Preserve the original function signature and attributes in decorator wrappers."""
20    def fix_signature(wrapper):
21        exec_scope = {}
22        parameters = formatargspec(*getargspec(func), formatvalue=lambda value: '')
23        exec 'def {0}{1}: return wrapper{1}'.format(func.__name__, parameters) in {'wrapper': wrapper}, exec_scope  # can't use tuple form here (see https://bugs.python.org/issue21591)
24        new_wrapper = exec_scope.pop(func.__name__)
25        new_wrapper.__name__ = func.__name__
26        new_wrapper.__doc__ = func.__doc__
27        new_wrapper.__module__ = func.__module__
28        new_wrapper.__defaults__ = func.__defaults__
29        new_wrapper.__dict__.update(func.__dict__)
30        return new_wrapper
31    return fix_signature
32
33
34@decorator
35def execute_once(func):
36    """Execute function/method once per function/instance"""
37
38    # noinspection PyUnusedLocal
39    @preserve_signature(func)
40    def check_arguments(*args, **kw):
41        pass
42
43    class ExecuteOnceMethodWrapper(object):
44        __slots__ = '__weakref__', '__method__', 'im_func_wrapper', 'called', 'lock'
45
46        def __init__(self, method, func_wrapper):
47            self.__method__ = method
48            self.im_func_wrapper = func_wrapper
49
50        def __call__(self, *args, **kw):
51            with self.im_func_wrapper.lock:
52                method = self.__method__
53                check_arguments.__get__(method.im_self, method.im_class)(*args, **kw)
54                instance = method.im_self if method.im_self is not None else args[0]
55                if self.im_func_wrapper.__callmap__.get(instance, False):
56                    return
57                self.im_func_wrapper.__callmap__[instance] = True
58                self.im_func_wrapper.__callmap__[method.im_class] = True
59                return method.__call__(*args, **kw)
60
61        def __dir__(self):
62            return sorted(set(dir(self.__method__) + dir(self.__class__) + list(self.__slots__)))
63
64        def __get__(self, obj, cls):
65            method = self.__method__.__get__(obj, cls)
66            return self.__class__(method, self.im_func_wrapper)
67
68        def __getattr__(self, name):
69            return getattr(self.__method__, name)
70
71        def __setattr__(self, name, value):
72            if name in self.__slots__:
73                object.__setattr__(self, name, value)
74            else:
75                setattr(self.__method__, name, value)
76
77        def __delattr__(self, name):
78            if name in self.__slots__:
79                object.__delattr__(self, name)
80            else:
81                delattr(self.__method__, name)
82
83        def __repr__(self):
84            return self.__method__.__repr__().replace('<', '<wrapper of ', 1)
85
86        @property
87        def called(self):
88            return self.im_func_wrapper.__callmap__.get(self.__method__.im_self if self.__method__.im_self is not None else self.__method__.im_class, False)
89
90        @property
91        def lock(self):
92            return self.im_func_wrapper.lock
93
94    class ExecuteOnceFunctionWrapper(object):
95        __slots__ = '__weakref__', '__func__', '__callmap__', 'called', 'lock'
96
97        # noinspection PyShadowingNames
98        def __init__(self, func):
99            self.__func__ = func
100            self.__callmap__ = weakobjectmap()
101            self.__callmap__[func] = False
102            self.lock = RLock()
103
104        def __call__(self, *args, **kw):
105            with self.lock:
106                check_arguments(*args, **kw)
107                if self.__callmap__[self.__func__]:
108                    return
109                self.__callmap__[self.__func__] = True
110                return self.__func__.__call__(*args, **kw)
111
112        def __dir__(self):
113            return sorted(set(dir(self.__func__) + dir(self.__class__) + list(self.__slots__)))
114
115        def __get__(self, obj, cls):
116            method = self.__func__.__get__(obj, cls)
117            return ExecuteOnceMethodWrapper(method, self)
118
119        def __getattr__(self, name):
120            return getattr(self.__func__, name)
121
122        def __setattr__(self, name, value):
123            if name in self.__slots__:
124                object.__setattr__(self, name, value)
125            else:
126                setattr(self.__func__, name, value)
127
128        def __delattr__(self, name):
129            if name in self.__slots__:
130                object.__delattr__(self, name)
131            else:
132                delattr(self.__func__, name)
133
134        def __repr__(self):
135            return self.__func__.__repr__().replace('<', '<wrapper of ', 1)
136
137        @property
138        def called(self):
139            return self.__callmap__[self.__func__]
140
141    return ExecuteOnceFunctionWrapper(func)
142
143
144__usage__ = """
145from application.python.decorator import decorator, preserve_signature
146
147# indicate that the next function will be used as a decorator (optional)
148@decorator
149def print_args(func):
150    @preserve_signature(func)
151    def wrapper(*args, **kw):
152        print('arguments: args={}, kw={}'.format(args, kw))
153        return func(*args, **kw)
154    return wrapper
155
156@print_args
157def foo(x, y, z=7):
158    return x + 3*y + z*z
159
160"""
161