1
# cython: linetrace=True
2
# distutils: define_macros=CYTHON_TRACE_NOGIL=1
8
from contextlib import contextmanager
10
from cpython.ref cimport PyObject, Py_INCREF, Py_XDECREF
12
cdef extern from "frameobject.h":
13
ctypedef struct PyFrameObject:
16
from cpython.pystate cimport (
18
PyTrace_CALL, PyTrace_EXCEPTION, PyTrace_LINE, PyTrace_RETURN,
19
PyTrace_C_CALL, PyTrace_C_EXCEPTION, PyTrace_C_RETURN)
22
void PyEval_SetProfile(Py_tracefunc cfunc, PyObject *obj)
23
void PyEval_SetTrace(Py_tracefunc cfunc, PyObject *obj)
28
PyTrace_EXCEPTION: 'exception',
30
PyTrace_RETURN: 'return',
31
PyTrace_C_CALL: 'ccall',
32
PyTrace_C_EXCEPTION: 'cexc',
33
PyTrace_C_RETURN: 'cret',
37
cdef int trace_trampoline(PyObject* _traceobj, PyFrameObject* _frame, int what, PyObject* _arg) except -1:
39
This is (more or less) what CPython does in sysmodule.c, function trace_trampoline().
43
if what == PyTrace_CALL:
46
callback = <object>_traceobj
48
callback = <object>_frame.f_trace
52
frame = <object>_frame
53
arg = <object>_arg if _arg else None
56
result = callback(frame, what, arg)
58
PyEval_SetTrace(NULL, NULL)
64
if result is not None:
65
# A bug in Py2.6 prevents us from calling the Python-level setter here,
66
# or otherwise we would get miscalculated line numbers. Was fixed in Py2.7.
69
_frame.f_trace = <PyObject*>result
75
def _create_trace_func(trace):
78
def _trace_func(frame, event, arg):
79
trace.append((frame.f_code.co_name, map_trace_types(event, event), frame.f_lineno - frame.f_code.co_firstlineno))
81
lnames = frame.f_code.co_varnames
82
if frame.f_code.co_name in local_names:
83
assert lnames == local_names[frame.f_code.co_name]
85
local_names[frame.f_code.co_name] = lnames
87
# Currently, the locals dict is empty for Cython code, but not for Python code.
88
if frame.f_code.co_name.startswith('py_'):
89
# Change this when we start providing proper access to locals.
90
assert frame.f_locals, frame.f_code.co_name
92
assert not frame.f_locals, frame.f_code.co_name
98
def _create_failing_call_trace_func(trace):
99
func = _create_trace_func(trace)
100
def _trace_func(frame, event, arg):
101
if event == PyTrace_CALL:
102
raise ValueError("failing call trace!")
104
func(frame, event, arg)
110
def _create__failing_line_trace_func(trace):
111
func = _create_trace_func(trace)
112
def _trace_func(frame, event, arg):
113
if event == PyTrace_LINE and trace:
114
if trace and trace[0] == frame.f_code.co_name:
115
# first line in the right function => fail!
116
raise ValueError("failing line trace!")
118
func(frame, event, arg)
123
def _create_disable_tracing(trace):
124
func = _create_trace_func(trace)
125
def _trace_func(frame, event, arg):
126
if frame.f_lineno - frame.f_code.co_firstlineno == 2:
127
PyEval_SetTrace(NULL, NULL)
130
func(frame, event, arg)
141
def cy_add_with_nogil(a,b):
142
cdef int z, x=a, y=b # 1
145
z += cy_add_nogil(x, y) # 4
149
def global_name(global_name):
150
# See GH #1836: accessing "frame.f_locals" deletes locals from globals dict.
151
return global_name + 321
154
cdef int cy_add_nogil(int a, int b) except -1 nogil:
159
def cy_try_except(func):
162
except KeyError as exc:
163
raise AttributeError(exc.args[0])
166
# CPython 3.11 has an issue when these Python functions are implemented inside of doctests and the trace function fails.
167
# https://github.com/python/cpython/issues/94381
168
plain_python_functions = {}
174
def py_add_with_nogil(a,b):
176
for _ in range(1): # 2
178
z += py_add(x, y) # 4
181
def py_return(retval=123): return retval
183
def py_try_except(func):
186
except KeyError as exc:
187
raise AttributeError(exc.args[0])
188
""", plain_python_functions)
193
was_enabled = gc.isenabled()
202
def run_trace(func, *args, bint with_sys=False):
204
>>> py_add = plain_python_functions['py_add']
205
>>> run_trace(py_add, 1, 2)
206
[('py_add', 'call', 0), ('py_add', 'line', 1), ('py_add', 'line', 2), ('py_add', 'return', 2)]
207
>>> run_trace(cy_add, 1, 2)
208
[('cy_add', 'call', 0), ('cy_add', 'line', 1), ('cy_add', 'line', 2), ('cy_add', 'return', 2)]
210
>>> run_trace(py_add, 1, 2, with_sys=True)
211
[('py_add', 'call', 0), ('py_add', 'line', 1), ('py_add', 'line', 2), ('py_add', 'return', 2)]
212
>>> run_trace(cy_add, 1, 2, with_sys=True)
213
[('cy_add', 'call', 0), ('cy_add', 'line', 1), ('cy_add', 'line', 2), ('cy_add', 'return', 2)]
215
>>> result = run_trace(cy_add_with_nogil, 1, 2)
217
[('cy_add_with_nogil', 'call', 0), ('cy_add_with_nogil', 'line', 1), ('cy_add_with_nogil', 'line', 2), ('cy_add_with_nogil', 'line', 3), ('cy_add_with_nogil', 'line', 4)]
219
[('cy_add_nogil', 'call', 0), ('cy_add_nogil', 'line', 1), ('cy_add_nogil', 'line', 2), ('cy_add_nogil', 'return', 2)]
221
[('cy_add_with_nogil', 'line', 2), ('cy_add_with_nogil', 'line', 5), ('cy_add_with_nogil', 'return', 5)]
223
>>> result = run_trace(cy_add_with_nogil, 1, 2, with_sys=True)
225
[('cy_add_with_nogil', 'call', 0), ('cy_add_with_nogil', 'line', 1), ('cy_add_with_nogil', 'line', 2), ('cy_add_with_nogil', 'line', 3), ('cy_add_with_nogil', 'line', 4)]
226
>>> result[5:9] # sys
227
[('cy_add_nogil', 'call', 0), ('cy_add_nogil', 'line', 1), ('cy_add_nogil', 'line', 2), ('cy_add_nogil', 'return', 2)]
229
[('cy_add_with_nogil', 'line', 2), ('cy_add_with_nogil', 'line', 5), ('cy_add_with_nogil', 'return', 5)]
231
>>> py_add_with_nogil = plain_python_functions['py_add_with_nogil']
232
>>> result = run_trace(py_add_with_nogil, 1, 2)
234
[('py_add_with_nogil', 'call', 0), ('py_add_with_nogil', 'line', 1), ('py_add_with_nogil', 'line', 2), ('py_add_with_nogil', 'line', 3), ('py_add_with_nogil', 'line', 4)]
236
[('py_add', 'call', 0), ('py_add', 'line', 1), ('py_add', 'line', 2), ('py_add', 'return', 2)]
238
[('py_add_with_nogil', 'line', 2), ('py_add_with_nogil', 'line', 5), ('py_add_with_nogil', 'return', 5)]
240
>>> run_trace(global_name, 123)
241
[('global_name', 'call', 0), ('global_name', 'line', 2), ('global_name', 'return', 2)]
242
>>> run_trace(global_name, 111)
243
[('global_name', 'call', 0), ('global_name', 'line', 2), ('global_name', 'return', 2)]
244
>>> run_trace(global_name, 111, with_sys=True)
245
[('global_name', 'call', 0), ('global_name', 'line', 2), ('global_name', 'return', 2)]
246
>>> run_trace(global_name, 111, with_sys=True)
247
[('global_name', 'call', 0), ('global_name', 'line', 2), ('global_name', 'return', 2)]
250
trace_func = _create_trace_func(trace)
253
sys.settrace(trace_func)
255
PyEval_SetTrace(<Py_tracefunc>trace_trampoline, <PyObject*>trace_func)
262
PyEval_SetTrace(NULL, NULL)
266
def run_trace_with_exception(func, bint with_sys=False, bint fail=False, call_func=cy_try_except):
268
>>> py_return = plain_python_functions["py_return"]
269
>>> run_trace_with_exception(py_return)
271
[('cy_try_except', 'call', 0), ('cy_try_except', 'line', 1), ('cy_try_except', 'line', 2), ('py_return', 'call', 0), ('py_return', 'line', 0), ('py_return', 'return', 0), ('cy_try_except', 'return', 2)]
272
>>> run_trace_with_exception(py_return, with_sys=True)
274
[('cy_try_except', 'call', 0), ('cy_try_except', 'line', 1), ('cy_try_except', 'line', 2), ('py_return', 'call', 0), ('py_return', 'line', 0), ('py_return', 'return', 0), ('cy_try_except', 'return', 2)]
276
>>> run_trace_with_exception(py_return, fail=True)
277
ValueError('failing line trace!')
278
[('cy_try_except', 'call', 0)]
280
#>>> run_trace_with_exception(lambda: 123, with_sys=True, fail=True)
282
#[('call', 0), ('line', 1), ('line', 2), ('call', 0), ('line', 0), ('return', 0), ('return', 2)]
284
>>> def py_raise_exc(exc=KeyError('huhu')): raise exc
285
>>> run_trace_with_exception(py_raise_exc)
286
AttributeError('huhu')
287
[('cy_try_except', 'call', 0), ('cy_try_except', 'line', 1), ('cy_try_except', 'line', 2), ('py_raise_exc', 'call', 0), ('py_raise_exc', 'line', 0), ('py_raise_exc', 'exception', 0), ('py_raise_exc', 'return', 0), ('cy_try_except', 'line', 3), ('cy_try_except', 'line', 4), ('cy_try_except', 'return', 4)]
288
>>> run_trace_with_exception(py_raise_exc, with_sys=True)
289
AttributeError('huhu')
290
[('cy_try_except', 'call', 0), ('cy_try_except', 'line', 1), ('cy_try_except', 'line', 2), ('py_raise_exc', 'call', 0), ('py_raise_exc', 'line', 0), ('py_raise_exc', 'exception', 0), ('py_raise_exc', 'return', 0), ('cy_try_except', 'line', 3), ('cy_try_except', 'line', 4), ('cy_try_except', 'return', 4)]
291
>>> run_trace_with_exception(py_raise_exc, fail=True)
292
ValueError('failing line trace!')
293
[('cy_try_except', 'call', 0)]
295
# Py3.9 issues a spurious additional line event after raising the final exception, so use +ELLIPSIS.
296
>>> py_try_except = plain_python_functions['py_try_except']
297
>>> run_trace_with_exception(py_raise_exc, call_func=py_try_except) # doctest: +ELLIPSIS
298
AttributeError('huhu')
299
[('py_try_except', 'call', 0), ('py_try_except', 'line', 1), ('py_try_except', 'line', 2), ('py_raise_exc', 'call', 0), ('py_raise_exc', 'line', 0), ('py_raise_exc', 'exception', 0), ('py_raise_exc', 'return', 0), ('py_try_except', 'exception', 2), ('py_try_except', 'line', 3), ('py_try_except', 'line', 4), ('py_try_except', 'exception', 4), ...('py_try_except', 'return', 4)]
300
>>> run_trace_with_exception(py_raise_exc, with_sys=True, call_func=py_try_except) # doctest: +ELLIPSIS
301
AttributeError('huhu')
302
[('py_try_except', 'call', 0), ('py_try_except', 'line', 1), ('py_try_except', 'line', 2), ('py_raise_exc', 'call', 0), ('py_raise_exc', 'line', 0), ('py_raise_exc', 'exception', 0), ('py_raise_exc', 'return', 0), ('py_try_except', 'exception', 2), ('py_try_except', 'line', 3), ('py_try_except', 'line', 4), ('py_try_except', 'exception', 4), ...('py_try_except', 'return', 4)]
303
>>> run_trace_with_exception(py_raise_exc, fail=True, call_func=py_try_except)
304
ValueError('failing line trace!')
305
[('py_try_except', 'call', 0)]
307
#>>> run_trace_with_exception(raise_exc, with_sys=True, fail=True)
309
#[('call', 0), ('line', 1), ('line', 2), ('call', 0), ('line', 0), ('exception', 0), ('return', 0), ('line', 3), ('line', 4), ('return', 4)]
311
trace = [call_func.__name__ if fail else 'NO ERROR']
312
trace_func = _create__failing_line_trace_func(trace) if fail else _create_trace_func(trace)
315
sys.settrace(trace_func)
317
PyEval_SetTrace(<Py_tracefunc>trace_trampoline, <PyObject*>trace_func)
320
retval = call_func(func)
321
except ValueError as exc:
322
print("%s(%r)" % (type(exc).__name__, str(exc)))
323
except AttributeError as exc:
324
print("%s(%r)" % (type(exc).__name__, str(exc)))
326
print('OK: %r' % retval)
331
PyEval_SetTrace(NULL, NULL)
335
def fail_on_call_trace(func, *args):
337
>>> py_add = plain_python_functions["py_add"]
338
>>> fail_on_call_trace(py_add, 1, 2)
339
Traceback (most recent call last):
340
ValueError: failing call trace!
342
>>> fail_on_call_trace(cy_add, 1, 2)
343
Traceback (most recent call last):
344
ValueError: failing call trace!
347
trace_func = _create_failing_call_trace_func(trace)
349
PyEval_SetTrace(<Py_tracefunc>trace_trampoline, <PyObject*>trace_func)
353
PyEval_SetTrace(NULL, NULL)
357
def fail_on_line_trace(fail_func, add_func, nogil_add_func):
359
>>> result = fail_on_line_trace(None, cy_add, cy_add_with_nogil)
363
['NO ERROR', ('cy_add', 'call', 0), ('cy_add', 'line', 1), ('cy_add', 'line', 2), ('cy_add', 'return', 2)]
365
[('cy_add_with_nogil', 'call', 0), ('cy_add_with_nogil', 'line', 1), ('cy_add_with_nogil', 'line', 2), ('cy_add_with_nogil', 'line', 3), ('cy_add_with_nogil', 'line', 4)]
367
[('cy_add_nogil', 'call', 0), ('cy_add_nogil', 'line', 1), ('cy_add_nogil', 'line', 2), ('cy_add_nogil', 'return', 2)]
369
[('cy_add_with_nogil', 'line', 2), ('cy_add_with_nogil', 'line', 5), ('cy_add_with_nogil', 'return', 5)]
371
>>> py_add = plain_python_functions["py_add"]
372
>>> py_add_with_nogil = plain_python_functions['py_add_with_nogil']
373
>>> result = fail_on_line_trace(None, py_add, py_add_with_nogil)
377
['NO ERROR', ('py_add', 'call', 0), ('py_add', 'line', 1), ('py_add', 'line', 2), ('py_add', 'return', 2)]
378
>>> result[5:10] # py
379
[('py_add_with_nogil', 'call', 0), ('py_add_with_nogil', 'line', 1), ('py_add_with_nogil', 'line', 2), ('py_add_with_nogil', 'line', 3), ('py_add_with_nogil', 'line', 4)]
380
>>> result[10:14] # py
381
[('py_add', 'call', 0), ('py_add', 'line', 1), ('py_add', 'line', 2), ('py_add', 'return', 2)]
383
[('py_add_with_nogil', 'line', 2), ('py_add_with_nogil', 'line', 5), ('py_add_with_nogil', 'return', 5)]
385
>>> result = fail_on_line_trace('cy_add_with_nogil', cy_add, cy_add_with_nogil)
388
['cy_add_with_nogil', ('cy_add', 'call', 0), ('cy_add', 'line', 1), ('cy_add', 'line', 2), ('cy_add', 'return', 2), ('cy_add_with_nogil', 'call', 0)]
390
>>> result = fail_on_line_trace('py_add_with_nogil', py_add, py_add_with_nogil) # py
393
['py_add_with_nogil', ('py_add', 'call', 0), ('py_add', 'line', 1), ('py_add', 'line', 2), ('py_add', 'return', 2), ('py_add_with_nogil', 'call', 0)]
395
>>> result = fail_on_line_trace('cy_add_nogil', cy_add, cy_add_with_nogil)
398
['cy_add_nogil', ('cy_add', 'call', 0), ('cy_add', 'line', 1), ('cy_add', 'line', 2), ('cy_add', 'return', 2)]
400
[('cy_add_with_nogil', 'call', 0), ('cy_add_with_nogil', 'line', 1), ('cy_add_with_nogil', 'line', 2), ('cy_add_with_nogil', 'line', 3), ('cy_add_with_nogil', 'line', 4), ('cy_add_nogil', 'call', 0)]
402
>>> result = fail_on_line_trace('py_add', py_add, py_add_with_nogil) # py
405
['py_add', ('py_add', 'call', 0), ('py_add', 'line', 1), ('py_add', 'line', 2), ('py_add', 'return', 2)]
407
[('py_add_with_nogil', 'call', 0), ('py_add_with_nogil', 'line', 1), ('py_add_with_nogil', 'line', 2), ('py_add_with_nogil', 'line', 3), ('py_add_with_nogil', 'line', 4), ('py_add', 'call', 0)]
412
trace_func = _create__failing_line_trace_func(trace)
414
PyEval_SetTrace(<Py_tracefunc>trace_trampoline, <PyObject*>trace_func)
420
trace[0] = fail_func # trigger error on first line
424
except Exception as exc:
427
PyEval_SetTrace(NULL, NULL)
435
def disable_trace(func, *args, bint with_sys=False):
437
>>> py_add = plain_python_functions["py_add"]
438
>>> disable_trace(py_add, 1, 2)
439
[('py_add', 'call', 0), ('py_add', 'line', 1)]
440
>>> disable_trace(py_add, 1, 2, with_sys=True)
441
[('py_add', 'call', 0), ('py_add', 'line', 1)]
443
>>> disable_trace(cy_add, 1, 2)
444
[('cy_add', 'call', 0), ('cy_add', 'line', 1)]
445
>>> disable_trace(cy_add, 1, 2, with_sys=True)
446
[('cy_add', 'call', 0), ('cy_add', 'line', 1)]
448
>>> disable_trace(cy_add_with_nogil, 1, 2)
449
[('cy_add_with_nogil', 'call', 0), ('cy_add_with_nogil', 'line', 1)]
450
>>> disable_trace(cy_add_with_nogil, 1, 2, with_sys=True)
451
[('cy_add_with_nogil', 'call', 0), ('cy_add_with_nogil', 'line', 1)]
454
trace_func = _create_disable_tracing(trace)
457
sys.settrace(trace_func)
459
PyEval_SetTrace(<Py_tracefunc>trace_trampoline, <PyObject*>trace_func)
466
PyEval_SetTrace(NULL, NULL)