Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Doc/includes/typestruct.h
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,7 @@ typedef struct _typeobject {
* Otherwise, limited to MAX_VERSIONS_PER_CLASS (defined elsewhere).
*/
uint16_t tp_versions_used;

/* call function for all referenced objects (includes non-cyclic refs) */
traverseproc tp_reachable;
} PyTypeObject;
3 changes: 3 additions & 0 deletions Include/cpython/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,9 @@ struct _typeobject {
* Otherwise, limited to MAX_VERSIONS_PER_CLASS (defined elsewhere).
*/
uint16_t tp_versions_used;

/* call function for all referenced objects (includes non-cyclic refs) */
traverseproc tp_reachable;
};

#define _Py_ATTR_CACHE_UNUSED (30000) // (see tp_versions_used)
Expand Down
3 changes: 3 additions & 0 deletions Include/internal/pycore_immutability.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ extern "C" {
# error "Py_BUILD_CORE must be defined to include this header"
#endif

typedef struct _Py_hashtable_t _Py_hashtable_t;

struct _Py_immutability_state {
PyObject *module_locks;
PyObject *blocking_on;
PyObject *freezable_types;
PyObject *destroy_cb;
_Py_hashtable_t *warned_types;
#ifdef Py_DEBUG
PyObject *traceback_func; // For debugging purposes, can be NULL
#endif
Expand Down
4 changes: 4 additions & 0 deletions Include/typeslots.h
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,7 @@
/* New in 3.14 */
#define Py_tp_token 83
#endif
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030F0000
/* New in 3.15 */
#define Py_tp_reachable 84
#endif
102 changes: 102 additions & 0 deletions Lib/test/test_freeze/test_reachable_warnings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""Tests for freeze warnings when tp_reachable is missing."""
import subprocess
import sys
import textwrap
import unittest


class TestReachableWarnings(unittest.TestCase):
"""Test that freeze logs warnings when tp_reachable is missing."""

def _run_code(self, code):
"""Run code in a subprocess and return (stdout, stderr)."""
result = subprocess.run(
[sys.executable, "-c", textwrap.dedent(code)],
capture_output=True,
text=True,
)
self.assertEqual(result.returncode, 0, result.stderr)
return result.stdout, result.stderr

def test_warn_tp_traverse_no_tp_reachable(self):
"""Warn once when a type has tp_traverse but no tp_reachable."""
stdout, stderr = self._run_code("""\
import _immutable

class MyClass:
def __init__(self):
self.x = 1

_immutable.register_freezable(MyClass)
_immutable._clear_tp_reachable(MyClass)

obj = MyClass()
_immutable.freeze(obj)
""")
self.assertIn(
"freeze: type 'MyClass' has tp_traverse but no tp_reachable",
stderr,
)

def test_warn_only_once_per_type(self):
"""A type should only produce the warning on the first freeze."""
stdout, stderr = self._run_code("""\
import _immutable

class MyClass:
def __init__(self):
self.x = 1

_immutable.register_freezable(MyClass)
_immutable._clear_tp_reachable(MyClass)

_immutable.freeze(MyClass())
_immutable.freeze(MyClass())
_immutable.freeze(MyClass())
""")
count = stderr.count(
"freeze: type 'MyClass' has tp_traverse but no tp_reachable"
)
self.assertEqual(count, 1, f"Expected 1 warning, got {count}:\n{stderr}")

def test_warn_different_types_separately(self):
"""Different types should each produce their own warning."""
stdout, stderr = self._run_code("""\
import _immutable

class ClassA:
pass

class ClassB:
pass

_immutable.register_freezable(ClassA)
_immutable.register_freezable(ClassB)
_immutable._clear_tp_reachable(ClassA)
_immutable._clear_tp_reachable(ClassB)

_immutable.freeze(ClassA())
_immutable.freeze(ClassB())
""")
self.assertIn("type 'ClassA'", stderr)
self.assertIn("type 'ClassB'", stderr)

def test_no_warning_with_tp_reachable(self):
"""No warning should appear when tp_reachable is present."""
stdout, stderr = self._run_code("""\
import _immutable

class MyClass:
def __init__(self):
self.x = 1

_immutable.register_freezable(MyClass)
# Do NOT clear tp_reachable
_immutable.freeze(MyClass())
""")
self.assertNotIn("tp_reachable", stderr)
self.assertNotIn("tp_traverse", stderr)


if __name__ == "__main__":
unittest.main()
18 changes: 18 additions & 0 deletions Modules/_immutablemodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,22 @@ PyType_Spec not_freezable_error_spec = {
.slots = not_freezable_error_slots,
};

/*
* Test helpers
*/

static PyObject *
immutable_clear_tp_reachable(PyObject *module, PyObject *obj)
{
if (!PyType_Check(obj)) {
PyErr_SetString(PyExc_TypeError, "Expected a type");
return NULL;
}
PyTypeObject *tp = (PyTypeObject *)obj;
tp->tp_reachable = NULL;
Py_RETURN_NONE;
}

/*
* MODULE
*/
Expand All @@ -143,6 +159,8 @@ static struct PyMethodDef immutable_methods[] = {
IMMUTABLE_REGISTER_FREEZABLE_METHODDEF
IMMUTABLE_FREEZE_METHODDEF
IMMUTABLE_ISFROZEN_METHODDEF
{"_clear_tp_reachable", immutable_clear_tp_reachable, METH_O,
"Clear tp_reachable on a type (test helper)."},
{ NULL, NULL }
};

Expand Down
16 changes: 16 additions & 0 deletions Objects/bytearrayobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -2740,6 +2740,13 @@ Construct a mutable bytearray object from:\n\
- any object implementing the buffer API.\n\
- an integer");

static int
bytearray_reachable(PyObject *self, visitproc visit, void *arg)
{
Py_VISIT(_PyObject_CAST(Py_TYPE(self)));
return 0;
}


static PyObject *bytearray_iter(PyObject *seq);

Expand Down Expand Up @@ -2784,6 +2791,7 @@ PyTypeObject PyByteArray_Type = {
PyType_GenericAlloc, /* tp_alloc */
PyType_GenericNew, /* tp_new */
PyObject_Free, /* tp_free */
.tp_reachable = bytearray_reachable,
.tp_version_tag = _Py_TYPE_VERSION_BYTEARRAY,
};

Expand Down Expand Up @@ -2918,6 +2926,13 @@ static PyMethodDef bytearrayiter_methods[] = {
{NULL, NULL} /* sentinel */
};

static int
bytearrayiter_reachable(PyObject *self, visitproc visit, void *arg)
{
Py_VISIT(_PyObject_CAST(Py_TYPE(self)));
return bytearrayiter_traverse(self, visit, arg);
}

PyTypeObject PyByteArrayIter_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"bytearray_iterator", /* tp_name */
Expand Down Expand Up @@ -2949,6 +2964,7 @@ PyTypeObject PyByteArrayIter_Type = {
bytearrayiter_next, /* tp_iternext */
bytearrayiter_methods, /* tp_methods */
0,
.tp_reachable = bytearrayiter_reachable,
};

static PyObject *
Expand Down
16 changes: 16 additions & 0 deletions Objects/bytesobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -1707,6 +1707,13 @@ bytes_subscript(PyObject *op, PyObject* item)
}
}

static int
bytes_reachable(PyObject *self, visitproc visit, void *arg)
{
Py_VISIT(_PyObject_CAST(Py_TYPE(self)));
return 0;
}

static int
bytes_buffer_getbuffer(PyObject *op, Py_buffer *view, int flags)
{
Expand Down Expand Up @@ -3157,6 +3164,7 @@ PyTypeObject PyBytes_Type = {
bytes_alloc, /* tp_alloc */
bytes_new, /* tp_new */
PyObject_Free, /* tp_free */
.tp_reachable = bytes_reachable,
.tp_version_tag = _Py_TYPE_VERSION_BYTES,
};

Expand Down Expand Up @@ -3399,6 +3407,13 @@ static PyMethodDef striter_methods[] = {
{NULL, NULL} /* sentinel */
};

static int
bytesiter_reachable(PyObject *self, visitproc visit, void *arg)
{
Py_VISIT(_PyObject_CAST(Py_TYPE(self)));
return striter_traverse(self, visit, arg);
}

PyTypeObject PyBytesIter_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"bytes_iterator", /* tp_name */
Expand Down Expand Up @@ -3430,6 +3445,7 @@ PyTypeObject PyBytesIter_Type = {
striter_next, /* tp_iternext */
striter_methods, /* tp_methods */
0,
.tp_reachable = bytesiter_reachable,
};

static PyObject *
Expand Down
8 changes: 8 additions & 0 deletions Objects/capsule.c
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,13 @@ capsule_traverse(PyObject *self, visitproc visit, void *arg)
return 0;
}

static int
capsule_reachable(PyObject *self, visitproc visit, void *arg)
{
Py_VISIT(_PyObject_CAST(Py_TYPE(self)));
return capsule_traverse(self, visit, arg);
}


static int
capsule_clear(PyObject *self)
Expand Down Expand Up @@ -361,6 +368,7 @@ PyTypeObject PyCapsule_Type = {
.tp_doc = PyCapsule_Type__doc__,
.tp_traverse = capsule_traverse,
.tp_clear = capsule_clear,
.tp_reachable = capsule_reachable,
};


8 changes: 8 additions & 0 deletions Objects/cellobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,13 @@ cell_traverse(PyObject *self, visitproc visit, void *arg)
return 0;
}

static int
cell_reachable(PyObject *self, visitproc visit, void *arg)
{
Py_VISIT(Py_TYPE(self));
return cell_traverse(self, visit, arg);
}

static int
cell_clear(PyObject *self)
{
Expand Down Expand Up @@ -218,4 +225,5 @@ PyTypeObject PyCell_Type = {
0, /* tp_alloc */
cell_new, /* tp_new */
0, /* tp_free */
.tp_reachable = cell_reachable,
};
15 changes: 15 additions & 0 deletions Objects/classobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,13 @@ method_traverse(PyObject *self, visitproc visit, void *arg)
return 0;
}

static int
method_reachable(PyObject *self, visitproc visit, void *arg)
{
Py_VISIT(_PyObject_CAST(Py_TYPE(self)));
return method_traverse(self, visit, arg);
}

static PyObject *
method_descr_get(PyObject *meth, PyObject *obj, PyObject *cls)
{
Expand All @@ -357,6 +364,7 @@ PyTypeObject PyMethod_Type = {
Py_TPFLAGS_HAVE_VECTORCALL,
.tp_doc = method_new__doc__,
.tp_traverse = method_traverse,
.tp_reachable = method_reachable,
.tp_richcompare = method_richcompare,
.tp_weaklistoffset = offsetof(PyMethodObject, im_weakreflist),
.tp_methods = method_methods,
Expand Down Expand Up @@ -456,6 +464,12 @@ instancemethod_traverse(PyObject *self, visitproc visit, void *arg) {
return 0;
}

static int
instancemethod_reachable(PyObject *self, visitproc visit, void *arg) {
Py_VISIT(_PyObject_CAST(Py_TYPE(self)));
return instancemethod_traverse(self, visit, arg);
}

static PyObject *
instancemethod_call(PyObject *self, PyObject *arg, PyObject *kw)
{
Expand Down Expand Up @@ -557,6 +571,7 @@ PyTypeObject PyInstanceMethod_Type = {
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,
.tp_doc = instancemethod_new__doc__,
.tp_traverse = instancemethod_traverse,
.tp_reachable = instancemethod_reachable,
.tp_richcompare = instancemethod_richcompare,
.tp_members = instancemethod_memberlist,
.tp_getset = instancemethod_getset,
Expand Down
Loading