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