summaryrefslogtreecommitdiff
path: root/src/leap/mail/decorators.py
blob: ae115f8fbc86197221225aae4ed0aa1c7488427c (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
# -*- coding: utf-8 -*-
# decorators.py
# Copyright (C) 2013 LEAP
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""
Useful decorators for mail package.
"""
import logging
import os

from functools import wraps

from twisted.internet.threads import deferToThread

logger = logging.getLogger(__name__)


# TODO
# Should write a helper to be able to pass a timeout argument.
# See this answer: http://stackoverflow.com/a/19019648/1157664
# And the notes by glyph and jpcalderone

def deferred_to_thread(f):
    """
    Decorator, for deferring methods to Threads.

    It will do a deferToThread of the decorated method
    unless the environment variable LEAPMAIL_DEBUG is set.

    It uses a descriptor to delay the definition of the
    method wrapper.
    """
    class descript(object):
        """
        The class to be used as decorator.

        It takes any method as the passed object.
        """

        def __init__(self, f):
            """
            Initializes the decorator object.

            :param f: the decorated function
            :type f: callable
            """
            self.f = f

        def __get__(self, instance, klass):
            """
            Descriptor implementation.

            At creation time, the decorated `method` is unbound.

            It will dispatch the make_unbound method if we still do not
            have an instance available, and the make_bound method when the
            method has already been bound to the instance.

            :param instance: the instance of the class, or None if not exist.
            :type instance: instantiated class or None.
            """
            if instance is None:
                # Class method was requested
                return self.make_unbound(klass)
            return self.make_bound(instance)

        def _errback(self, failure):
            """
            Errorback that logs the exception catched.

            :param failure: a twisted failure
            :type failure: Failure
            """
            logger.warning('Error in method: %s' % (self.f.__name__))
            logger.exception(failure.getTraceback())

        def make_unbound(self, klass):
            """
            Return a wrapped function with the unbound call, during the
            early access to the decortad method. This gets passed
            only the class (not the instance since it does not yet exist).

            :param klass: the class to which the still unbound method belongs
            :type klass: type
            """

            @wraps(self.f)
            def wrapper(*args, **kwargs):
                """
                We're temporarily wrapping the decorated method, but this
                should not be called, since our application should use
                the bound-wrapped method after this decorator class has been
                used.

                This documentation will vanish at runtime.
                """
                raise TypeError(
                    'unbound method {}() must be called with {} instance '
                    'as first argument (got nothing instead)'.format(
                        self.f.__name__,
                        klass.__name__)
                )
            return wrapper

        def make_bound(self, instance):
            """
            Return a function that wraps the bound method call,
            after we are able to access the instance object.

            :param instance: an instance of the class the decorated method,
                             now bound, belongs to.
            :type instance: object
            """

            @wraps(self.f)
            def wrapper(*args, **kwargs):
                """
                Do a proper function wrapper that defers the decorated method
                call to a separated thread if the LEAPMAIL_DEBUG
                environment variable is set.

                This documentation will vanish at runtime.
                """
                if not os.environ.get('LEAPMAIL_DEBUG'):
                    d = deferToThread(self.f, instance, *args, **kwargs)
                    d.addErrback(self._errback)
                    return d
                else:
                    return self.f(instance, *args, **kwargs)

            # This instance does not need the descriptor anymore,
            # let it find the wrapper directly next time:
            setattr(instance, self.f.__name__, wrapper)
            return wrapper

    return descript(f)