# cython: language_level=3, auto_pickle=False from cpython.ref cimport PyObject, Py_INCREF, Py_DECREF, Py_XDECREF, Py_XINCREF from cpython.exc cimport PyErr_Fetch, PyErr_Restore from cpython.pystate cimport PyThreadState_Get cimport cython loglevel = 0 reflog = [] cdef log(level, action, obj, lineno): if loglevel >= level: reflog.append((lineno, action, id(obj))) LOG_NONE, LOG_ALL = range(2) @cython.final cdef class Context(object): cdef readonly object name, filename cdef readonly dict refs cdef readonly list errors cdef readonly Py_ssize_t start def __cinit__(self, name, line=0, filename=None): self.name = name self.start = line self.filename = filename self.refs = {} # id -> (count, [lineno]) self.errors = [] cdef regref(self, obj, lineno, bint is_null): log(LOG_ALL, u'regref', u"" if is_null else obj, lineno) if is_null: self.errors.append(f"NULL argument on line {lineno}") return id_ = id(obj) count, linenumbers = self.refs.get(id_, (0, [])) self.refs[id_] = (count + 1, linenumbers) linenumbers.append(lineno) cdef bint delref(self, obj, lineno, bint is_null) except -1: # returns whether it is ok to do the decref operation log(LOG_ALL, u'delref', u"" if is_null else obj, lineno) if is_null: self.errors.append(f"NULL argument on line {lineno}") return False id_ = id(obj) count, linenumbers = self.refs.get(id_, (0, [])) if count == 0: self.errors.append(f"Too many decrefs on line {lineno}, reference acquired on lines {linenumbers!r}") return False elif count == 1: del self.refs[id_] return True else: self.refs[id_] = (count - 1, linenumbers) return True cdef end(self): if self.refs: msg = u"References leaked:" for count, linenos in self.refs.itervalues(): msg += f"\n ({count}) acquired on lines: {u', '.join([f'{x}' for x in linenos])}" self.errors.append(msg) if self.errors: return u"\n".join([u'REFNANNY: '+error for error in self.errors]) else: return None cdef void report_unraisable(object e=None): try: if e is None: import sys e = sys.exc_info()[1] print(f"refnanny raised an exception: {e}") except: pass # We absolutely cannot exit with an exception # All Python operations must happen after any existing # exception has been fetched, in case we are called from # exception-handling code. cdef PyObject* SetupContext(char* funcname, int lineno, char* filename) except NULL: if Context is None: # Context may be None during finalize phase. # In that case, we don't want to be doing anything fancy # like caching and resetting exceptions. return NULL cdef (PyObject*) type = NULL, value = NULL, tb = NULL, result = NULL PyThreadState_Get() PyErr_Fetch(&type, &value, &tb) try: ctx = Context(funcname, lineno, filename) Py_INCREF(ctx) result = ctx except Exception, e: report_unraisable(e) PyErr_Restore(type, value, tb) return result cdef void GOTREF(PyObject* ctx, PyObject* p_obj, int lineno): if ctx == NULL: return cdef (PyObject*) type = NULL, value = NULL, tb = NULL PyErr_Fetch(&type, &value, &tb) try: try: if p_obj is NULL: (ctx).regref(None, lineno, True) else: (ctx).regref(p_obj, lineno, False) except: report_unraisable() except: # __Pyx_GetException may itself raise errors pass PyErr_Restore(type, value, tb) cdef int GIVEREF_and_report(PyObject* ctx, PyObject* p_obj, int lineno): if ctx == NULL: return 1 cdef (PyObject*) type = NULL, value = NULL, tb = NULL cdef bint decref_ok = False PyErr_Fetch(&type, &value, &tb) try: try: if p_obj is NULL: decref_ok = (ctx).delref(None, lineno, True) else: decref_ok = (ctx).delref(p_obj, lineno, False) except: report_unraisable() except: # __Pyx_GetException may itself raise errors pass PyErr_Restore(type, value, tb) return decref_ok cdef void GIVEREF(PyObject* ctx, PyObject* p_obj, int lineno): GIVEREF_and_report(ctx, p_obj, lineno) cdef void INCREF(PyObject* ctx, PyObject* obj, int lineno): Py_XINCREF(obj) PyThreadState_Get() GOTREF(ctx, obj, lineno) cdef void DECREF(PyObject* ctx, PyObject* obj, int lineno): if GIVEREF_and_report(ctx, obj, lineno): Py_XDECREF(obj) PyThreadState_Get() cdef void FinishContext(PyObject** ctx): if ctx == NULL or ctx[0] == NULL: return cdef (PyObject*) type = NULL, value = NULL, tb = NULL cdef object errors = None cdef Context context PyThreadState_Get() PyErr_Fetch(&type, &value, &tb) try: try: context = ctx[0] errors = context.end() if errors: print(f"{context.filename.decode('latin1')}: {context.name.decode('latin1')}()") print(errors) context = None except: report_unraisable() except: # __Pyx_GetException may itself raise errors pass Py_XDECREF(ctx[0]) ctx[0] = NULL PyErr_Restore(type, value, tb) ctypedef struct RefNannyAPIStruct: void (*INCREF)(PyObject*, PyObject*, int) void (*DECREF)(PyObject*, PyObject*, int) void (*GOTREF)(PyObject*, PyObject*, int) void (*GIVEREF)(PyObject*, PyObject*, int) PyObject* (*SetupContext)(char*, int, char*) except NULL void (*FinishContext)(PyObject**) cdef RefNannyAPIStruct api api.INCREF = INCREF api.DECREF = DECREF api.GOTREF = GOTREF api.GIVEREF = GIVEREF api.SetupContext = SetupContext api.FinishContext = FinishContext cdef extern from "Python.h": object PyLong_FromVoidPtr(void*) RefNannyAPI = PyLong_FromVoidPtr(&api)