Completed
Push — master ( 0878c0...da4845 )
by Ionel Cristian
01:09
created

src.hunter.CallPrinter.__call__()   C

Complexity

Conditions 8

Size

Total Lines 59

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 59
rs 5.8915
cc 8

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 0
168
169
        self.stream.write("{thread:{thread_align}}{filename}{:>{align}}{colon}:{lineno}{:<5} {kind}{:9} {code}{}{reset}\n".format(
170
            self._format_filename(event),
171
            event.lineno,
172
            event.kind,
173
            lines[0],
174
            thread=thread_name, thread_align=thread_align,
175
            align=self.filename_alignment,
176
            code=self.code_colors[event.kind],
177
            **self.event_colors
178
        ))
179
        for line in lines[1:]:
180
            self.stream.write("{thread:{thread_align}}{:>{align}}       {kind}{:9} {code}{}{reset}\n".format(
181
                "",
182
                r"   |",
183
                line,
184
                thread=thread_name, thread_align=thread_align,
185
                align=self.filename_alignment,
186
                code=self.code_colors[event.kind],
187
                **self.event_colors
188
            ))
189
190
        if event.kind in ('return', 'exception'):
191
            self.stream.write("{thread:{thread_align}}{:>{align}}       {continuation}{:9} {color}{} value: {detail}{}{reset}\n".format(
192
                "",
193
                "...",
194
                event.kind,
195
                self._safe_repr(event.arg),
196
                thread=thread_name, thread_align=thread_align,
197
                align=self.filename_alignment,
198
                color=self.event_colors[event.kind],
199
                **self.event_colors
200
            ))
201
202
203
class CallPrinter(CodePrinter):
204
    """
205
    An action that just prints the code being executed, but unlike :obj:`hunter.CodePrinter` it indents based on
206
    callstack depth and it also shows ``repr()`` of function arguments.
207
208
    Args:
209
        stream (file-like): Stream to write to. Default: ``sys.stderr``.
210
        filename_alignment (int): Default size for the filename column (files are right-aligned). Default: ``40``.
211
        force_colors (bool): Force coloring. Default: ``False``.
212
        repr_limit (bool): Limit length of ``repr()`` output. Default: ``512``.
213
214
    .. versionadded:: 1.2.0
215
216
    .. note::
217
218
        This will be the default action in `hunter 2.0`.
219
    """
220
221
    def __init__(self, **options):
222
        super(CallPrinter, self).__init__(**options)
223
        self.locals = defaultdict(list)
224
225
    def __call__(self, event, sep=os.path.sep, join=os.path.join):
226
        """
227
        Handle event and print filename, line number and source code. If event.kind is a `return` or `exception` also
228
        prints values.
229
        """
230
        filename = self._format_filename(event)
231
        ident = event.module, event.function
232
        thread = threading.current_thread()
233
        thread_name = thread.name if event.tracer.threading_support else ''
234
        thread_align = self.thread_alignment if event.tracer.threading_support else 0
235
        stack = self.locals[thread.ident]
236
237
        if event.kind == 'call':
238
            code = event.code
239
            stack.append(ident)
240
            self.stream.write(
241
                "{thread:{thread_align}}{filename}{:>{align}}{colon}:{lineno}{:<5} {kind}{:9} {}{call}=>{normal} "
242
                "{}({}{call}{normal}){reset}\n".format(
243
                    filename,
244
                    event.lineno,
245
                    event.kind,
246
                    '   ' * (len(stack) - 1),
247
                    event.function,
248
                    ', '.join('{vars}{vars-name}{0}{vars}={reset}{1}'.format(
249
                        var,
250
                        self._safe_repr(event.locals.get(var, MISSING)),
251
                        **self.event_colors
252
                    ) for var in code.co_varnames[:code.co_argcount]),
253
                    thread=thread_name, thread_align=thread_align,
254
                    align=self.filename_alignment,
255
                    **self.event_colors
256
                ))
257
        elif event.kind in ('return', 'exception'):
258
            self.stream.write("{thread:{thread_align}}{filename}{:>{align}}{colon}:{lineno}{:<5} {kind}{:9} {code}{}{}{normal} {}: {reset}{}\n".format(
259
                filename,
260
                event.lineno,
261
                event.kind,
262
                '   ' * (len(stack) - 1),
263
                {'return': '<=', 'exception': '<!'}[event.kind],
264
                event.function,
265
                self._safe_repr(event.arg),
266
                thread=thread_name, thread_align=thread_align,
267
                align=self.filename_alignment,
268
                code=self.event_colors[event.kind],
269
                **self.event_colors
270
            ))
271
            if stack and stack[-1] == ident:
272
                stack.pop()
273
        else:
274
            self.stream.write("{thread:{thread_align}}{filename}{:>{align}}{colon}:{lineno}{:<5} {kind}{:9} {reset}{}{}\n".format(
275
                filename,
276
                event.lineno,
277
                event.kind,
278
                '   ' * len(stack),
279
                event.source.strip(),
280
                thread=thread_name, thread_align=thread_align,
281
                align=self.filename_alignment,
282
                code=self.code_colors[event.kind],
283
                **self.event_colors
284
            ))
285
286
287
class VarsPrinter(Fields.names.globals.stream.filename_alignment, ColorStreamAction):
288
    """
289
    An action that prints local variables and optionally global variables visible from the current executing frame.
290
291
    Args:
292
        *names (strings): Names to evaluate. Expressions can be used (will only try to evaluate if all the variables are
293
            present on the frame.
294
        globals (bool): Allow access to globals. Default: ``False`` (only looks at locals).
295
        stream (file-like): Stream to write to. Default: ``sys.stderr``.
296
        filename_alignment (int): Default size for the filename column (files are right-aligned). Default: ``40``.
297
        force_colors (bool): Force coloring. Default: ``False``.
298
        repr_limit (bool): Limit length of ``repr()`` output. Default: ``512``.
299
300
    .. note::
301
        This is the default action.
302
303
    .. warning::
304
305
        In `hunter 2.0` the default action will be :obj:`hunter.CallPrinter`.
306
    """
307
308
    def __init__(self, *names, **options):
309
        if not names:
310
            raise TypeError("VarsPrinter requires at least one variable name/expression.")
311
        self.names = {
312
            name: set(self._iter_symbols(name))
313
            for name in names
314
        }
315
        self.globals = options.pop('globals', False)
316
        super(VarsPrinter, self).__init__(**options)
317
318
    @staticmethod
319
    def _iter_symbols(code):
320
        """
321
        Iterate all the variable names in the given expression.
322
323
        Example:
324
325
        * ``self.foobar`` yields ``self``
326
        * ``self[foobar]`` yields `self`` and ``foobar``
327
        """
328
        for node in ast.walk(ast.parse(code)):
329
            if isinstance(node, ast.Name):
330
                yield node.id
331
332
    def _safe_eval(self, code, event):
333
        """
334
        Try to evaluate the given code on the given frame. If failure occurs, returns some ugly string with exception.
335
        """
336
        try:
337
            return eval(code, event.globals if self.globals else {}, event.locals)
338
        except Exception as exc:
339
            return "{internal-failure}FAILED EVAL: {internal-detail}{!r}".format(exc, **self.event_colors)
340
341
    def __call__(self, event):
342
        """
343
        Handle event and print the specified variables.
344
        """
345
        first = True
346
        frame_symbols = set(event.locals)
347
        if self.globals:
348
            frame_symbols |= set(event.globals)
349
        thread_name = threading.current_thread().name if event.tracer.threading_support else ''
350
        thread_align = self.thread_alignment if event.tracer.threading_support else 0
351
352
        for code, symbols in self.names.items():
353
            try:
354
                obj = eval(code, event.globals if self.globals else {}, event.locals)
355
            except AttributeError:
356
                continue
357
            except Exception as exc:
358
                printout = "{internal-failure}FAILED EVAL: {internal-detail}{!r}".format(exc, **self.event_colors)
359
            else:
360
                printout = self._safe_repr(obj)
361
362
            if frame_symbols >= symbols:
363
                self.stream.write("{thread:{thread_align}}{:>{align}}       {vars}{:9} {vars-name}{} {vars}=> {reset}{}{reset}\n".format(
364
                    "",
365
                    "vars" if first else "...",
366
                    code,
367
                    printout,
368
                    thread=thread_name, thread_align=thread_align,
369
                    align=self.filename_alignment,
370
                    **self.event_colors
371
                ))
372
                first = False
373