File: twisted.py

package info (click to toggle)
python-eliot 1.16.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 964 kB
  • sloc: python: 8,641; makefile: 151
file content (265 lines) | stat: -rw-r--r-- 8,185 bytes parent folder | download
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
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
"""
APIs for using Eliot from Twisted.
"""

import os
import sys

from twisted.logger import Logger as TwistedLogger
from twisted.python.failure import Failure
from twisted.internet.defer import inlineCallbacks

from ._action import current_action
from . import addDestination
from ._generators import eliot_friendly_generator_function

__all__ = [
    "AlreadyFinished",
    "DeferredContext",
    "redirectLogsForTrial",
    "inline_callbacks",
]


def _passthrough(result):
    return result


class AlreadyFinished(Exception):
    """
    L{DeferredContext.addCallbacks} or similar method was called after
    L{DeferredContext.addActionFinish}.

    This indicates a programming bug, e.g. forgetting to unwrap the
    underlying L{Deferred} when passing on to some other piece of code that
    doesn't care about the action context.
    """


class DeferredContext(object):
    """
    A L{Deferred} equivalent of L{eliot.Action.context} and
    L{eliot.action.finish}.

    Makes a L{Deferred}'s callbacks run in a L{eliot.Action}'s context, and
    allows indicating which callbacks to wait for before the action is
    finished.

    The action to use will be taken from the call context.

    @ivar result: The wrapped L{Deferred}.
    """

    def __init__(self, deferred):
        """
        @param deferred: L{twisted.internet.defer.Deferred} to wrap.
        """
        self.result = deferred
        self._action = current_action()
        self._finishAdded = False
        if self._action is None:
            raise RuntimeError(
                "DeferredContext() should only be created in the context of "
                "an eliot.Action."
            )

    def addCallbacks(
        self,
        callback,
        errback=None,
        callbackArgs=None,
        callbackKeywords=None,
        errbackArgs=None,
        errbackKeywords=None,
    ):
        """
        Add a pair of callbacks that will be run in the context of an eliot
        action.

        @return: C{self}
        @rtype: L{DeferredContext}

        @raises AlreadyFinished: L{DeferredContext.addActionFinish} has been
            called. This indicates a programmer error.
        """
        if self._finishAdded:
            raise AlreadyFinished()

        if errback is None:
            errback = _passthrough

        def callbackWithContext(*args, **kwargs):
            return self._action.run(callback, *args, **kwargs)

        def errbackWithContext(*args, **kwargs):
            return self._action.run(errback, *args, **kwargs)

        self.result.addCallbacks(
            callbackWithContext,
            errbackWithContext,
            callbackArgs,
            callbackKeywords,
            errbackArgs,
            errbackKeywords,
        )
        return self

    def addCallback(self, callback, *args, **kw):
        """
        Add a success callback that will be run in the context of an eliot
        action.

        @return: C{self}
        @rtype: L{DeferredContext}

        @raises AlreadyFinished: L{DeferredContext.addActionFinish} has been
            called. This indicates a programmer error.
        """
        return self.addCallbacks(
            callback, _passthrough, callbackArgs=args, callbackKeywords=kw
        )

    def addErrback(self, errback, *args, **kw):
        """
        Add a failure callback that will be run in the context of an eliot
        action.

        @return: C{self}
        @rtype: L{DeferredContext}

        @raises AlreadyFinished: L{DeferredContext.addActionFinish} has been
            called. This indicates a programmer error.
        """
        return self.addCallbacks(
            _passthrough, errback, errbackArgs=args, errbackKeywords=kw
        )

    def addBoth(self, callback, *args, **kw):
        """
        Add a single callback as both success and failure callbacks.

        @return: C{self}
        @rtype: L{DeferredContext}

        @raises AlreadyFinished: L{DeferredContext.addActionFinish} has been
            called. This indicates a programmer error.
        """
        return self.addCallbacks(callback, callback, args, kw, args, kw)

    def addActionFinish(self):
        """
        Indicates all callbacks that should run within the action's context
        have been added, and that the action should therefore finish once
        those callbacks have fired.

        @return: The wrapped L{Deferred}.

        @raises AlreadyFinished: L{DeferredContext.addActionFinish} has been
            called previously. This indicates a programmer error.
        """
        if self._finishAdded:
            raise AlreadyFinished()
        self._finishAdded = True

        def done(result):
            if isinstance(result, Failure):
                exception = result.value
            else:
                exception = None
            self._action.finish(exception)
            return result

        self.result.addBoth(done)
        return self.result


class TwistedDestination(object):
    """
    An Eliot logging destination that forwards logs to Twisted's logging.

    Do not use if you're also redirecting Twisted's logs to Eliot, since then
    you'll have an infinite loop.
    """

    def __init__(self):
        self._logger = TwistedLogger(namespace="eliot")

    def __call__(self, message):
        """
        Log an Eliot message to Twisted's log.

        @param message: A rendered Eliot message.
        @type message: L{dict}
        """
        if message.get("message_type") == "eliot:traceback":
            method = self._logger.critical
        else:
            method = self._logger.info
        method(format="Eliot message: {eliot}", eliot=message)


class _RedirectLogsForTrial(object):
    """
    When called inside a I{trial} process redirect Eliot log messages to
    Twisted's logging system, otherwise do nothing.

    This allows reading Eliot logs output by running unit tests with
    I{trial} in its normal log location: C{_trial_temp/test.log}.

    The way you use it is by calling it a module level in some module that will
    be loaded by trial, typically the top-level C{__init__.py} of your package.

    This function can usually be safely called in all programs since it will
    have no side-effects if used outside of trial. The only exception is you
    are redirecting Twisted logs to Eliot; you should make sure not call
    this function in that case so as to prevent infinite loops. In addition,
    calling the function multiple times has the same effect as calling it
    once.

    (This is not thread-safe at the moment, so in theory multiple threads
    calling this might result in multiple destinatios being added - see
    https://github.com/itamarst/eliot/issues/78).

    Currently this works by checking if C{sys.argv[0]} is called C{trial};
    the ideal mechanism would require
    https://twistedmatrix.com/trac/ticket/6939 to be fixed, but probably
    there are better solutions even without that -
    https://github.com/itamarst/eliot/issues/76 covers those.

    @ivar _sys: An object similar to, and typically identical to, Python's
        L{sys} module.

    @ivar _redirected: L{True} if trial logs have been redirected once already.
    """

    def __init__(self, sys):
        self._sys = sys
        self._redirected = False

    def __call__(self):
        """
        Do the redirect if necessary.

        @return: The destination added to Eliot if any, otherwise L{None}.
        """
        if os.path.basename(self._sys.argv[0]) == "trial" and not self._redirected:
            self._redirected = True
            destination = TwistedDestination()
            addDestination(destination)
            return destination


redirectLogsForTrial = _RedirectLogsForTrial(sys)


def inline_callbacks(original, debug=False):
    """
    Decorate a function like ``inlineCallbacks`` would but in a more
    Eliot-friendly way.  Use it just like ``inlineCallbacks`` but where you
    want Eliot action contexts to Do The Right Thing inside the decorated
    function.
    """
    f = eliot_friendly_generator_function(original)
    if debug:
        f.debug = True
    return inlineCallbacks(f)