1# cython: linetrace=True
2# distutils: define_macros=CYTHON_TRACE_NOGIL=1
3# mode: run
4# tag: trace
5
6import sys
7
8from cpython.ref cimport PyObject, Py_INCREF, Py_XDECREF
9
10cdef extern from "frameobject.h":
11    ctypedef struct PyFrameObject:
12        PyObject *f_trace
13
14from cpython.pystate cimport (
15    Py_tracefunc,
16    PyTrace_CALL, PyTrace_EXCEPTION, PyTrace_LINE, PyTrace_RETURN,
17    PyTrace_C_CALL, PyTrace_C_EXCEPTION, PyTrace_C_RETURN)
18
19cdef extern from *:
20    void PyEval_SetProfile(Py_tracefunc cfunc, PyObject *obj)
21    void PyEval_SetTrace(Py_tracefunc cfunc, PyObject *obj)
22
23
24map_trace_types = {
25    PyTrace_CALL:        'call',
26    PyTrace_EXCEPTION:   'exception',
27    PyTrace_LINE:        'line',
28    PyTrace_RETURN:      'return',
29    PyTrace_C_CALL:      'ccall',
30    PyTrace_C_EXCEPTION: 'cexc',
31    PyTrace_C_RETURN:    'cret',
32}.get
33
34
35cdef int trace_trampoline(PyObject* _traceobj, PyFrameObject* _frame, int what, PyObject* _arg) except -1:
36    """
37    This is (more or less) what CPython does in sysmodule.c, function trace_trampoline().
38    """
39    cdef PyObject *tmp
40
41    if what == PyTrace_CALL:
42        if _traceobj is NULL:
43            return 0
44        callback = <object>_traceobj
45    elif _frame.f_trace:
46        callback = <object>_frame.f_trace
47    else:
48        return 0
49
50    frame = <object>_frame
51    arg = <object>_arg if _arg else None
52
53    try:
54        result = callback(frame, what, arg)
55    except:
56        PyEval_SetTrace(NULL, NULL)
57        tmp = _frame.f_trace
58        _frame.f_trace = NULL
59        Py_XDECREF(tmp)
60        raise
61
62    if result is not None:
63        # A bug in Py2.6 prevents us from calling the Python-level setter here,
64        # or otherwise we would get miscalculated line numbers. Was fixed in Py2.7.
65        tmp = _frame.f_trace
66        Py_INCREF(result)
67        _frame.f_trace = <PyObject*>result
68        Py_XDECREF(tmp)
69
70    return 0
71
72
73def _create_trace_func(trace):
74    local_names = {}
75
76    def _trace_func(frame, event, arg):
77        if sys.version_info < (3,) and 'line_trace' not in frame.f_code.co_filename:
78            # Prevent tracing into Py2 doctest functions.
79            return None
80
81        trace.append((map_trace_types(event, event), frame.f_lineno - frame.f_code.co_firstlineno))
82
83        lnames = frame.f_code.co_varnames
84        if frame.f_code.co_name in local_names:
85            assert lnames == local_names[frame.f_code.co_name]
86        else:
87            local_names[frame.f_code.co_name] = lnames
88
89        # Currently, the locals dict is empty for Cython code, but not for Python code.
90        if frame.f_code.co_name.startswith('py_'):
91            # Change this when we start providing proper access to locals.
92            assert frame.f_locals, frame.f_code.co_name
93        else:
94            assert not frame.f_locals, frame.f_code.co_name
95
96        return _trace_func
97    return _trace_func
98
99
100def _create_failing_call_trace_func(trace):
101    func = _create_trace_func(trace)
102    def _trace_func(frame, event, arg):
103        if event == PyTrace_CALL:
104            raise ValueError("failing call trace!")
105
106        func(frame, event, arg)
107        return _trace_func
108
109    return _trace_func
110
111
112def _create__failing_line_trace_func(trace):
113    func = _create_trace_func(trace)
114    def _trace_func(frame, event, arg):
115        if event == PyTrace_LINE and trace:
116            if trace and trace[0] == frame.f_code.co_name:
117                # first line in the right function => fail!
118                raise ValueError("failing line trace!")
119
120        func(frame, event, arg)
121        return _trace_func
122    return _trace_func
123
124
125def _create_disable_tracing(trace):
126    func = _create_trace_func(trace)
127    def _trace_func(frame, event, arg):
128        if frame.f_lineno - frame.f_code.co_firstlineno == 2:
129            PyEval_SetTrace(NULL, NULL)
130            return None
131
132        func(frame, event, arg)
133        return _trace_func
134
135    return _trace_func
136
137
138def cy_add(a,b):
139    x = a + b     # 1
140    return x      # 2
141
142
143def cy_add_with_nogil(a,b):
144    cdef int z, x=a, y=b         # 1
145    with nogil:                  # 2
146        z = 0                    # 3
147        z += cy_add_nogil(x, y)  # 4
148    return z                     # 5
149
150
151def global_name(global_name):
152    # See GH #1836: accessing "frame.f_locals" deletes locals from globals dict.
153    return global_name + 321
154
155
156cdef int cy_add_nogil(int a, int b) nogil except -1:
157    x = a + b   # 1
158    return x    # 2
159
160
161def cy_try_except(func):
162    try:
163        return func()
164    except KeyError as exc:
165        raise AttributeError(exc.args[0])
166
167
168def run_trace(func, *args, bint with_sys=False):
169    """
170    >>> def py_add(a,b):
171    ...     x = a+b
172    ...     return x
173
174    >>> def py_add_with_nogil(a,b):
175    ...     x=a; y=b                     # 1
176    ...     for _ in range(1):           # 2
177    ...         z = 0                    # 3
178    ...         z += py_add(x, y)        # 4
179    ...     return z                     # 5
180
181    >>> run_trace(py_add, 1, 2)
182    [('call', 0), ('line', 1), ('line', 2), ('return', 2)]
183    >>> run_trace(cy_add, 1, 2)
184    [('call', 0), ('line', 1), ('line', 2), ('return', 2)]
185
186    >>> run_trace(py_add, 1, 2, with_sys=True)
187    [('call', 0), ('line', 1), ('line', 2), ('return', 2)]
188    >>> run_trace(cy_add, 1, 2, with_sys=True)
189    [('call', 0), ('line', 1), ('line', 2), ('return', 2)]
190
191    >>> result = run_trace(cy_add_with_nogil, 1, 2)
192    >>> result[:5]
193    [('call', 0), ('line', 1), ('line', 2), ('line', 3), ('line', 4)]
194    >>> result[5:9]
195    [('call', 0), ('line', 1), ('line', 2), ('return', 2)]
196    >>> result[9:]
197    [('line', 2), ('line', 5), ('return', 5)]
198
199    >>> result = run_trace(cy_add_with_nogil, 1, 2, with_sys=True)
200    >>> result[:5]  # sys
201    [('call', 0), ('line', 1), ('line', 2), ('line', 3), ('line', 4)]
202    >>> result[5:9]  # sys
203    [('call', 0), ('line', 1), ('line', 2), ('return', 2)]
204    >>> result[9:]  # sys
205    [('line', 2), ('line', 5), ('return', 5)]
206
207    >>> result = run_trace(py_add_with_nogil, 1, 2)
208    >>> result[:5]  # py
209    [('call', 0), ('line', 1), ('line', 2), ('line', 3), ('line', 4)]
210    >>> result[5:9]  # py
211    [('call', 0), ('line', 1), ('line', 2), ('return', 2)]
212    >>> result[9:]  # py
213    [('line', 2), ('line', 5), ('return', 5)]
214
215    >>> run_trace(global_name, 123)
216    [('call', 0), ('line', 2), ('return', 2)]
217    >>> run_trace(global_name, 111)
218    [('call', 0), ('line', 2), ('return', 2)]
219    >>> run_trace(global_name, 111, with_sys=True)
220    [('call', 0), ('line', 2), ('return', 2)]
221    >>> run_trace(global_name, 111, with_sys=True)
222    [('call', 0), ('line', 2), ('return', 2)]
223    """
224    trace = []
225    trace_func = _create_trace_func(trace)
226    if with_sys:
227        sys.settrace(trace_func)
228    else:
229        PyEval_SetTrace(<Py_tracefunc>trace_trampoline, <PyObject*>trace_func)
230    try:
231        func(*args)
232    finally:
233        if with_sys:
234            sys.settrace(None)
235        else:
236            PyEval_SetTrace(NULL, NULL)
237    return trace
238
239
240def run_trace_with_exception(func, bint with_sys=False, bint fail=False):
241    """
242    >>> def py_return(retval=123): return retval
243    >>> run_trace_with_exception(py_return)
244    OK: 123
245    [('call', 0), ('line', 1), ('line', 2), ('call', 0), ('line', 0), ('return', 0), ('return', 2)]
246    >>> run_trace_with_exception(py_return, with_sys=True)
247    OK: 123
248    [('call', 0), ('line', 1), ('line', 2), ('call', 0), ('line', 0), ('return', 0), ('return', 2)]
249
250    >>> run_trace_with_exception(py_return, fail=True)
251    ValueError('failing line trace!')
252    [('call', 0)]
253
254    #>>> run_trace_with_exception(lambda: 123, with_sys=True, fail=True)
255    #ValueError('huhu')
256    #[('call', 0), ('line', 1), ('line', 2), ('call', 0), ('line', 0), ('return', 0), ('return', 2)]
257
258    >>> def py_raise_exc(exc=KeyError('huhu')): raise exc
259    >>> run_trace_with_exception(py_raise_exc)
260    AttributeError('huhu')
261    [('call', 0), ('line', 1), ('line', 2), ('call', 0), ('line', 0), ('exception', 0), ('return', 0), ('line', 3), ('line', 4), ('return', 4)]
262    >>> run_trace_with_exception(py_raise_exc, with_sys=True)
263    AttributeError('huhu')
264    [('call', 0), ('line', 1), ('line', 2), ('call', 0), ('line', 0), ('exception', 0), ('return', 0), ('line', 3), ('line', 4), ('return', 4)]
265    >>> run_trace_with_exception(py_raise_exc, fail=True)
266    ValueError('failing line trace!')
267    [('call', 0)]
268
269    #>>> run_trace_with_exception(raise_exc, with_sys=True, fail=True)
270    #ValueError('huhu')
271    #[('call', 0), ('line', 1), ('line', 2), ('call', 0), ('line', 0), ('exception', 0), ('return', 0), ('line', 3), ('line', 4), ('return', 4)]
272    """
273    trace = ['cy_try_except' if fail else 'NO ERROR']
274    trace_func = _create__failing_line_trace_func(trace) if fail else _create_trace_func(trace)
275    if with_sys:
276        sys.settrace(trace_func)
277    else:
278        PyEval_SetTrace(<Py_tracefunc>trace_trampoline, <PyObject*>trace_func)
279    try:
280        try:
281            retval = cy_try_except(func)
282        except ValueError as exc:
283            print("%s(%r)" % (type(exc).__name__, str(exc)))
284        except AttributeError as exc:
285            print("%s(%r)" % (type(exc).__name__, str(exc)))
286        else:
287            print('OK: %r' % retval)
288    finally:
289        if with_sys:
290            sys.settrace(None)
291        else:
292            PyEval_SetTrace(NULL, NULL)
293    return trace[1:]
294
295
296def fail_on_call_trace(func, *args):
297    """
298    >>> def py_add(a,b):
299    ...     x = a+b
300    ...     return x
301
302    >>> fail_on_call_trace(py_add, 1, 2)
303    Traceback (most recent call last):
304    ValueError: failing call trace!
305
306    >>> fail_on_call_trace(cy_add, 1, 2)
307    Traceback (most recent call last):
308    ValueError: failing call trace!
309    """
310    trace = []
311    trace_func = _create_failing_call_trace_func(trace)
312    PyEval_SetTrace(<Py_tracefunc>trace_trampoline, <PyObject*>trace_func)
313    try:
314        func(*args)
315    finally:
316        PyEval_SetTrace(NULL, NULL)
317    assert not trace
318
319
320def fail_on_line_trace(fail_func, add_func, nogil_add_func):
321    """
322    >>> def py_add(a,b):
323    ...     x = a+b       # 1
324    ...     return x      # 2
325
326    >>> def py_add_with_nogil(a,b):
327    ...     x=a; y=b                     # 1
328    ...     for _ in range(1):           # 2
329    ...         z = 0                    # 3
330    ...         z += py_add(x, y)        # 4
331    ...     return z                     # 5
332
333    >>> result = fail_on_line_trace(None, cy_add, cy_add_with_nogil)
334    >>> len(result)
335    17
336    >>> result[:5]
337    ['NO ERROR', ('call', 0), ('line', 1), ('line', 2), ('return', 2)]
338    >>> result[5:10]
339    [('call', 0), ('line', 1), ('line', 2), ('line', 3), ('line', 4)]
340    >>> result[10:14]
341    [('call', 0), ('line', 1), ('line', 2), ('return', 2)]
342    >>> result[14:]
343    [('line', 2), ('line', 5), ('return', 5)]
344
345    >>> result = fail_on_line_trace(None, py_add, py_add_with_nogil)
346    >>> len(result)
347    17
348    >>> result[:5]  # py
349    ['NO ERROR', ('call', 0), ('line', 1), ('line', 2), ('return', 2)]
350    >>> result[5:10]  # py
351    [('call', 0), ('line', 1), ('line', 2), ('line', 3), ('line', 4)]
352    >>> result[10:14]  # py
353    [('call', 0), ('line', 1), ('line', 2), ('return', 2)]
354    >>> result[14:]  # py
355    [('line', 2), ('line', 5), ('return', 5)]
356
357    >>> result = fail_on_line_trace('cy_add_with_nogil', cy_add, cy_add_with_nogil)
358    failing line trace!
359    >>> result
360    ['cy_add_with_nogil', ('call', 0), ('line', 1), ('line', 2), ('return', 2), ('call', 0)]
361
362    >>> result = fail_on_line_trace('py_add_with_nogil', py_add, py_add_with_nogil)  # py
363    failing line trace!
364    >>> result  # py
365    ['py_add_with_nogil', ('call', 0), ('line', 1), ('line', 2), ('return', 2), ('call', 0)]
366
367    >>> result = fail_on_line_trace('cy_add_nogil', cy_add, cy_add_with_nogil)
368    failing line trace!
369    >>> result[:5]
370    ['cy_add_nogil', ('call', 0), ('line', 1), ('line', 2), ('return', 2)]
371    >>> result[5:]
372    [('call', 0), ('line', 1), ('line', 2), ('line', 3), ('line', 4), ('call', 0)]
373
374    >>> result = fail_on_line_trace('py_add', py_add, py_add_with_nogil)  # py
375    failing line trace!
376    >>> result[:5]  # py
377    ['py_add', ('call', 0), ('line', 1), ('line', 2), ('return', 2)]
378    >>> result[5:]  # py
379    [('call', 0), ('line', 1), ('line', 2), ('line', 3), ('line', 4), ('call', 0)]
380    """
381    cdef int x = 1
382    trace = ['NO ERROR']
383    exception = None
384    trace_func = _create__failing_line_trace_func(trace)
385    PyEval_SetTrace(<Py_tracefunc>trace_trampoline, <PyObject*>trace_func)
386    try:
387        x += 1
388        add_func(1, 2)
389        x += 1
390        if fail_func:
391            trace[0] = fail_func  # trigger error on first line
392        x += 1
393        nogil_add_func(3, 4)
394        x += 1
395    except Exception as exc:
396        exception = str(exc)
397    finally:
398        PyEval_SetTrace(NULL, NULL)
399    if exception:
400        print(exception)
401    else:
402        assert x == 5
403    return trace
404
405
406def disable_trace(func, *args, bint with_sys=False):
407    """
408    >>> def py_add(a,b):
409    ...     x = a+b
410    ...     return x
411    >>> disable_trace(py_add, 1, 2)
412    [('call', 0), ('line', 1)]
413    >>> disable_trace(py_add, 1, 2, with_sys=True)
414    [('call', 0), ('line', 1)]
415
416    >>> disable_trace(cy_add, 1, 2)
417    [('call', 0), ('line', 1)]
418    >>> disable_trace(cy_add, 1, 2, with_sys=True)
419    [('call', 0), ('line', 1)]
420
421    >>> disable_trace(cy_add_with_nogil, 1, 2)
422    [('call', 0), ('line', 1)]
423    >>> disable_trace(cy_add_with_nogil, 1, 2, with_sys=True)
424    [('call', 0), ('line', 1)]
425    """
426    trace = []
427    trace_func = _create_disable_tracing(trace)
428    if with_sys:
429        sys.settrace(trace_func)
430    else:
431        PyEval_SetTrace(<Py_tracefunc>trace_trampoline, <PyObject*>trace_func)
432    try:
433        func(*args)
434    finally:
435        if with_sys:
436            sys.settrace(None)
437        else:
438            PyEval_SetTrace(NULL, NULL)
439    return trace
440