Completed
Push — master ( 82185e...4d55e8 )
by Ionel Cristian
01:08
created

src.hunter.ColorStreamAction   A

Complexity

Total Complexity 12

Size/Duplication

Total Lines 54
Duplicated Lines 0 %
Metric Value
wmc 12
dl 0
loc 54
rs 10

3 Methods

Rating   Name   Duplication   Size   Complexity  
A __init__() 0 11 1
A _safe_repr() 0 13 3
A stream() 0 3 1
1
from __future__ import absolute_import
2
3
import ast
4
import os
5
import pdb
6
import sys
7
import threading
8
from collections import defaultdict
9
from itertools import chain
10
11
from colorama import AnsiToWin32
12
from colorama import Back
13
from colorama import Fore
14
from colorama import Style
15
from six import string_types
16
17
from .util import Fields
18
19
20
EVENT_COLORS = {
21
    'reset': Style.RESET_ALL,
22
    'normal': Style.NORMAL,
23
    'filename': '',
24
    'colon': Style.BRIGHT + Fore.BLACK,
25
    'lineno': Style.RESET_ALL,
26
    'kind': Fore.CYAN,
27
    'continuation': Style.BRIGHT + Fore.BLUE,
28
    'call': Style.BRIGHT + Fore.BLUE,
29
    'return': Style.BRIGHT + Fore.GREEN,
30
    'exception': Style.BRIGHT + Fore.RED,
31
    'detail': Style.NORMAL,
32
    'vars': Style.RESET_ALL + Fore.MAGENTA,
33
    'vars-name': Style.BRIGHT,
34
    'internal-failure': Style.BRIGHT + Back.RED + Fore.RED,
35
    'internal-detail': Fore.WHITE,
36
    'source-failure': Style.BRIGHT + Back.YELLOW + Fore.YELLOW,
37
    'source-detail': Fore.WHITE,
38
}
39
CODE_COLORS = {
40
    'call': Fore.RESET + Style.BRIGHT,
41
    'line': Fore.RESET,
42
    'return': Fore.YELLOW,
43
    'exception': Fore.RED,
44
}
45
NO_COLORS = {key: '' for key in chain(CODE_COLORS, EVENT_COLORS)}
46
MISSING = type('MISSING', (), {'__repr__': lambda _: '?'})()
47
48
49
class Action(object):
50
    def __call__(self, event):
51
        raise NotImplementedError()
52
53
54
class Debugger(Fields.klass.kwargs, Action):
55
    """
56
    An action that starts ``pdb``.
57
    """
58
59
    def __init__(self, klass=pdb.Pdb, **kwargs):
60
        self.klass = klass
61
        self.kwargs = kwargs
62
63
    def __call__(self, event):
64
        """
65
        Runs a ``pdb.set_trace`` at the matching frame.
66
        """
67
        self.klass(**self.kwargs).set_trace(event.frame)
68
69
70
class ColorStreamAction(Fields.stream.force_colors.filename_alignment.thread_alignment.repr_limit, Action):
71
    _stream_cache = {}
72
    _stream = None
73
    _tty = None
74
75
    def __init__(self,
76
                 stream=sys.stderr,
77
                 force_colors=False,
78
                 filename_alignment=40,
79
                 thread_alignment=12,
80
                 repr_limit=1024):
81
        self.force_colors = force_colors
82
        self.stream = stream
83
        self.filename_alignment = filename_alignment
84
        self.thread_alignment = thread_alignment
85
        self.repr_limit = repr_limit
86
87
    @property
88
    def stream(self):
89
        return self._stream
90
91
    @stream.setter
92
    def stream(self, value):
93
        if isinstance(value, string_types):
94
            if value in self._stream_cache:
95
                value = self._stream_cache[value]
96
            else:
97
                value = self._stream_cache[value] = open(value, 'a', buffering=0)
98
99
        isatty = getattr(value, 'isatty', None)
100
        if self.force_colors or (isatty and isatty() and os.name != 'java'):
101
            self._stream = AnsiToWin32(value, strip=False)
102
            self._tty = True
103
            self.event_colors = EVENT_COLORS
104
            self.code_colors = CODE_COLORS
105
        else:
106
            self._tty = False
107
            self._stream = value
108
            self.event_colors = NO_COLORS
109
            self.code_colors = NO_COLORS
110
111
    def _safe_repr(self, obj):
112
        limit = self.repr_limit
113
114
        try:
115
            s = repr(obj)
116
            s = s.replace('\n', r'\n')
117
            if len(s) > limit:
118
                cutoff = limit // 2
119
                return "{} {continuation}[...]{reset} {}".format(s[:cutoff], s[-cutoff:], **self.event_colors)
120
            else:
121
                return s
122
        except Exception as exc:
123
            return "{internal-failure}!!! FAILED REPR: {internal-detail}{!r}{reset}".format(exc, **self.event_colors)
124
125
126
class CodePrinter(ColorStreamAction):
127
    """
128
    An action that just prints the code being executed.
129
130
    Args:
131
        stream (file-like): Stream to write to. Default: ``sys.stderr``.
132
        filename_alignment (int): Default size for the filename column (files are right-aligned). Default: ``40``.
133
        force_colors (bool): Force coloring. Default: ``False``.
134
        repr_limit (bool): Limit length of ``repr()`` output. Default: ``512``.
135
    """
136
    def _safe_source(self, event):
137
        try:
138
            lines = event._raw_fullsource.rstrip().splitlines()
139
            if lines:
140
                return lines
141
            else:
142
                return "{source-failure}??? NO SOURCE: {source-detail}" \
143
                       "Source code string for module {!r} is empty.".format(event.module, **self.event_colors),
144
            return lines
145
        except Exception as exc:
146
            return "{source-failure}??? NO SOURCE: {source-detail}{!r}".format(exc, **self.event_colors),
147
148
    def _format_filename(self, event):
149
        filename = event.filename or "<???>"
150
        if len(filename) > self.filename_alignment:
151
            filename = '[...]{}'.format(filename[5 - self.filename_alignment:])
152
        return filename
153
154
    def __call__(self, event, sep=os.path.sep, join=os.path.join):
155
        """
156
        Handle event and print filename, line number and source code. If event.kind is a `return` or `exception` also
157
        prints values.
158
        """
159
160
        # context = event.tracer
161
        # alignment = context.filename_alignment = max(
162
        #     getattr(context, 'filename_alignment', 5),
163
        #     len(filename)
164
        # )
165
        lines = self._safe_source(event)
166
        thread_name = threading.current_thread().name if event.tracer.threading_support else ''
167
        thread_align = self.thread_alignment if event.tracer.threading_support else ''
168
169
        self.stream.write(
170
            "{thread:{thread_align}}{filename}{:>{align}}{colon}:{lineno}{:<5} {kind}{:9} {code}{}{reset}\n".format(
171
                self._format_filename(event),
172
                event.lineno,
173
                event.kind,
174
                lines[0],
175
                thread=thread_name, thread_align=thread_align,
176
                align=self.filename_alignment,
177
                code=self.code_colors[event.kind],
178
                **self.event_colors
179
            ))
180
        for line in lines[1:]:
181
            self.stream.write("{thread:{thread_align}}{:>{align}}       {kind}{:9} {code}{}{reset}\n".format(
182
                "",
183
                r"   |",
184
                line,
185
                thread=thread_name, thread_align=thread_align,
186
                align=self.filename_alignment,
187
                code=self.code_colors[event.kind],
188
                **self.event_colors
189
            ))
190
191
        if event.kind in ('return', 'exception'):
192
            self.stream.write(
193
                "{thread:{thread_align}}{:>{align}}       {continuation}{:9} {color}{} "
194
                "value: {detail}{}{reset}\n".format(
195
                    "",
196
                    "...",
197
                    event.kind,
198
                    self._safe_repr(event.arg),
199
                    thread=thread_name, thread_align=thread_align,
200
                    align=self.filename_alignment,
201
                    color=self.event_colors[event.kind],
202
                    **self.event_colors
203
                ))
204
205
206
class CallPrinter(CodePrinter):
207
    """
208
    An action that just prints the code being executed, but unlike :obj:`hunter.CodePrinter` it indents based on
209
    callstack depth and it also shows ``repr()`` of function arguments.
210
211
    Args:
212
        stream (file-like): Stream to write to. Default: ``sys.stderr``.
213
        filename_alignment (int): Default size for the filename column (files are right-aligned). Default: ``40``.
214
        force_colors (bool): Force coloring. Default: ``False``.
215
        repr_limit (bool): Limit length of ``repr()`` output. Default: ``512``.
216
217
    .. versionadded:: 1.2.0
218
219
    .. note::
220
221
        This will be the default action in `hunter 2.0`.
222
    """
223
224
    def __init__(self, **options):
225
        super(CallPrinter, self).__init__(**options)
226
        self.locals = defaultdict(list)
227
228
    def __call__(self, event, sep=os.path.sep, join=os.path.join):
229
        """
230
        Handle event and print filename, line number and source code. If event.kind is a `return` or `exception` also
231
        prints values.
232
        """
233
        filename = self._format_filename(event)
234
        ident = event.module, event.function
235
        thread = threading.current_thread()
236
        thread_name = thread.name if event.tracer.threading_support else ''
237
        thread_align = self.thread_alignment if event.tracer.threading_support else ''
238
        stack = self.locals[thread.ident]
239
240
        if event.kind == 'call':
241
            code = event.code
242
            stack.append(ident)
243
            self.stream.write(
244
                "{thread:{thread_align}}{filename}{:>{align}}{colon}:{lineno}{:<5} {kind}{:9} {}{call}=>{normal} "
245
                "{}({}{call}{normal}){reset}\n".format(
246
                    filename,
247
                    event.lineno,
248
                    event.kind,
249
                    '   ' * (len(stack) - 1),
250
                    event.function,
251
                    ', '.join('{vars}{vars-name}{0}{vars}={reset}{1}'.format(
252
                        var,
253
                        self._safe_repr(event.locals.get(var, MISSING)),
254
                        **self.event_colors
255
                    ) for var in code.co_varnames[:code.co_argcount]),
256
                    thread=thread_name, thread_align=thread_align,
257
                    align=self.filename_alignment,
258
                    **self.event_colors
259
                ))
260
        elif event.kind in ('return', 'exception'):
261
            self.stream.write(
262
                "{thread:{thread_align}}{filename}{:>{align}}{colon}:{lineno}{:<5} {kind}{:9} "
263
                "{code}{}{}{normal} {}: {reset}{}\n".format(
264
                    filename,
265
                    event.lineno,
266
                    event.kind,
267
                    '   ' * (len(stack) - 1),
268
                    {'return': '<=', 'exception': '<!'}[event.kind],
269
                    event.function,
270
                    self._safe_repr(event.arg),
271
                    thread=thread_name, thread_align=thread_align,
272
                    align=self.filename_alignment,
273
                    code=self.event_colors[event.kind],
274
                    **self.event_colors
275
                ))
276
            if stack and stack[-1] == ident:
277
                stack.pop()
278
        else:
279
            self.stream.write(
280
                "{thread:{thread_align}}{filename}{:>{align}}{colon}:{lineno}{:<5} {kind}{:9} {reset}{}{}\n".format(
281
                    filename,
282
                    event.lineno,
283
                    event.kind,
284
                    '   ' * len(stack),
285
                    event.source.strip(),
286
                    thread=thread_name, thread_align=thread_align,
287
                    align=self.filename_alignment,
288
                    code=self.code_colors[event.kind],
289
                    **self.event_colors
290
                ))
291
292
293
class VarsPrinter(Fields.names.globals.stream.filename_alignment, ColorStreamAction):
294
    """
295
    An action that prints local variables and optionally global variables visible from the current executing frame.
296
297
    Args:
298
        *names (strings): Names to evaluate. Expressions can be used (will only try to evaluate if all the variables are
299
            present on the frame.
300
        globals (bool): Allow access to globals. Default: ``False`` (only looks at locals).
301
        stream (file-like): Stream to write to. Default: ``sys.stderr``.
302
        filename_alignment (int): Default size for the filename column (files are right-aligned). Default: ``40``.
303
        force_colors (bool): Force coloring. Default: ``False``.
304
        repr_limit (bool): Limit length of ``repr()`` output. Default: ``512``.
305
306
    .. note::
307
        This is the default action.
308
309
    .. warning::
310
311
        In `hunter 2.0` the default action will be :obj:`hunter.CallPrinter`.
312
    """
313
314
    def __init__(self, *names, **options):
315
        if not names:
316
            raise TypeError("VarsPrinter requires at least one variable name/expression.")
317
        self.names = {
318
            name: set(self._iter_symbols(name))
319
            for name in names
320
        }
321
        self.globals = options.pop('globals', False)
322
        super(VarsPrinter, self).__init__(**options)
323
324
    @staticmethod
325
    def _iter_symbols(code):
326
        """
327
        Iterate all the variable names in the given expression.
328
329
        Example:
330
331
        * ``self.foobar`` yields ``self``
332
        * ``self[foobar]`` yields `self`` and ``foobar``
333
        """
334
        for node in ast.walk(ast.parse(code)):
335
            if isinstance(node, ast.Name):
336
                yield node.id
337
338
    def _safe_eval(self, code, event):
339
        """
340
        Try to evaluate the given code on the given frame. If failure occurs, returns some ugly string with exception.
341
        """
342
        try:
343
            return eval(code, event.globals if self.globals else {}, event.locals)
344
        except Exception as exc:
345
            return "{internal-failure}FAILED EVAL: {internal-detail}{!r}".format(exc, **self.event_colors)
346
347
    def __call__(self, event):
348
        """
349
        Handle event and print the specified variables.
350
        """
351
        first = True
352
        frame_symbols = set(event.locals)
353
        if self.globals:
354
            frame_symbols |= set(event.globals)
355
        thread_name = threading.current_thread().name if event.tracer.threading_support else ''
356
        thread_align = self.thread_alignment if event.tracer.threading_support else ''
357
358
        for code, symbols in self.names.items():
359
            try:
360
                obj = eval(code, event.globals if self.globals else {}, event.locals)
361
            except AttributeError:
362
                continue
363
            except Exception as exc:
364
                printout = "{internal-failure}FAILED EVAL: {internal-detail}{!r}".format(exc, **self.event_colors)
365
            else:
366
                printout = self._safe_repr(obj)
367
368
            if frame_symbols >= symbols:
369
                self.stream.write("{thread:{thread_align}}{:>{align}}       {vars}{:9} {vars-name}{} {vars}=> {reset}{}{reset}\n".format(
370
                    "",
371
                    "vars" if first else "...",
372
                    code,
373
                    printout,
374
                    thread=thread_name, thread_align=thread_align,
375
                    align=self.filename_alignment,
376
                    **self.event_colors
377
                ))
378
                first = False
379