Index: Lib/test/test_memoryio.py =================================================================== --- Lib/test/test_memoryio.py (revision 73313) +++ Lib/test/test_memoryio.py (working copy) @@ -9,7 +9,56 @@ import io import _pyio as pyio import sys +import functools +#XXX: Should check_if_leaking and LeakTestMeta be moved to test.support? + +def check_if_leaking(testfunc): + """Decorator for checking reference leak regressions. + Based roughly on the code in regrtest. + """ + @functools.wraps(testfunc) + @unittest.skipIf(not hasattr(sys, "gettotalrefcount"), + "Reference leaks tracking requires debug build") + def wrapper(*args, **kwargs): + repcount = 8 + nwarmup = 3 + deltas = [] + for i in range(repcount): + rc = sys.gettotalrefcount() + testfunc(*args, **kwargs) + support.gc_collect() + if i >= nwarmup: + deltas.append(sys.gettotalrefcount() - rc - 2) + if any(deltas): + raise AssertionError( + "{0} leaked: {1}".format(testfunc.__name__, deltas)) + return wrapper + +class LeakTestMeta(type): + """Metaclass for adding reference leak checking to all the test methods + of a class. + + This particularly useful for test cases composed uniquely of fine-grained + reference leak tests. + """ + + def __new__(cls, name, bases, dict): + for attribute in dict: + if (attribute.startswith("test") and + hasattr(dict[attribute], "__call__")): + dict[attribute] = check_if_leaking(dict[attribute]) + return type(name, bases, dict) + + +class RefleakTestMixin(metaclass=LeakTestMeta): + + def test_instance_dict_leak(self): + # Issue #6242 + memio = self.ioclass() + memio.foo = 1 + + class MemorySeekTestMixin: def testInit(self): @@ -339,7 +388,8 @@ self.assertEqual(test2(), buf) -class PyBytesIOTest(MemoryTestMixin, MemorySeekTestMixin, unittest.TestCase): +class PyBytesIOTest(MemoryTestMixin, MemorySeekTestMixin, RefleakTestMixin, + unittest.TestCase): UnsupportedOperation = pyio.UnsupportedOperation @@ -417,7 +467,8 @@ self.assertEqual(memio.getvalue(), buf) -class PyStringIOTest(MemoryTestMixin, MemorySeekTestMixin, unittest.TestCase): +class PyStringIOTest(MemoryTestMixin, MemorySeekTestMixin, RefleakTestMixin, + unittest.TestCase): buftype = str ioclass = pyio.StringIO UnsupportedOperation = pyio.UnsupportedOperation Index: Modules/_io/bytesio.c =================================================================== --- Modules/_io/bytesio.c (revision 73313) +++ Modules/_io/bytesio.c (working copy) @@ -606,14 +606,31 @@ Py_RETURN_NONE; } +static int +bytesio_traverse(BytesIOObject *self, visitproc visit, void *arg) +{ + Py_VISIT(self->dict); + return 0; +} + +static int +bytesio_clear(BytesIOObject *self) +{ + Py_CLEAR(self->dict); + return 0; +} + static void bytesio_dealloc(BytesIOObject *self) { - if (self->buf != NULL) { + _PyObject_GC_UNTRACK(self); + if (self->buf) { PyMem_Free(self->buf); self->buf = NULL; } - Py_TYPE(self)->tp_clear((PyObject *)self); + Py_CLEAR(self->dict); + if (self->weakreflist != NULL) + PyObject_ClearWeakRefs((PyObject *)self); Py_TYPE(self)->tp_free(self); } @@ -663,24 +680,6 @@ return 0; } -static int -bytesio_traverse(BytesIOObject *self, visitproc visit, void *arg) -{ - Py_VISIT(self->dict); - Py_VISIT(self->weakreflist); - return 0; -} - -static int -bytesio_clear(BytesIOObject *self) -{ - Py_CLEAR(self->dict); - if (self->weakreflist != NULL) - PyObject_ClearWeakRefs((PyObject *)self); - return 0; -} - - static PyGetSetDef bytesio_getsetlist[] = { {"closed", (getter)bytesio_get_closed, NULL, "True if the file is closed."}, Index: Modules/_io/stringio.c =================================================================== --- Modules/_io/stringio.c (revision 73313) +++ Modules/_io/stringio.c (working copy) @@ -509,11 +509,15 @@ stringio_dealloc(StringIOObject *self) { _PyObject_GC_UNTRACK(self); + self->ok = 0; + if (self->buf) { + PyMem_Free(self->buf); + self->buf = NULL; + } Py_CLEAR(self->readnl); Py_CLEAR(self->writenl); Py_CLEAR(self->decoder); - if (self->buf) - PyMem_Free(self->buf); + Py_CLEAR(self->dict); if (self->weakreflist != NULL) PyObject_ClearWeakRefs((PyObject *) self); Py_TYPE(self)->tp_free(self);