Skip to content
Open
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
89 changes: 89 additions & 0 deletions Lib/test/test_freeze/test_reachable_warnings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""Tests for freeze warnings when tp_reachable is missing.

Uses the _test_reachable C extension which provides two static types:

HasTraverseNoReachable – has tp_traverse, tp_reachable deliberately NULL
NoTraverseNoReachable – neither tp_traverse nor tp_reachable
"""
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 when a C type has tp_traverse but no tp_reachable."""
stdout, stderr = self._run_code("""\
import _immutable, _test_reachable
obj = _test_reachable.HasTraverseNoReachable(42)
_immutable.freeze(obj)
""")
self.assertIn(
"freeze: type '_test_reachable.HasTraverseNoReachable' "
"has tp_traverse but no tp_reachable",
stderr,
)

def test_warn_no_traverse_no_reachable(self):
"""Warn when a C type has neither tp_traverse nor tp_reachable."""
stdout, stderr = self._run_code("""\
import _immutable, _test_reachable
obj = _test_reachable.NoTraverseNoReachable()
_immutable.freeze(obj)
""")
self.assertIn(
"freeze: type '_test_reachable.NoTraverseNoReachable' "
"has no tp_traverse and 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, _test_reachable
_immutable.freeze(_test_reachable.HasTraverseNoReachable(1))
_immutable.freeze(_test_reachable.HasTraverseNoReachable(2))
_immutable.freeze(_test_reachable.HasTraverseNoReachable(3))
""")
msg = (
"freeze: type '_test_reachable.HasTraverseNoReachable' "
"has tp_traverse but no tp_reachable"
)
count = stderr.count(msg)
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, _test_reachable
_immutable.freeze(_test_reachable.HasTraverseNoReachable(1))
_immutable.freeze(_test_reachable.NoTraverseNoReachable())
""")
self.assertIn("HasTraverseNoReachable", stderr)
self.assertIn("NoTraverseNoReachable", stderr)

def test_no_warning_with_tp_reachable(self):
"""No warning for a type that has tp_reachable set."""
stdout, stderr = self._run_code("""\
import _immutable, _test_reachable
obj = _test_reachable.HasReachable(42)
_immutable.freeze(obj)
""")
self.assertNotIn("HasReachable", stderr)


if __name__ == "__main__":
unittest.main()
1 change: 1 addition & 0 deletions Modules/Setup.stdlib.in
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@
@MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/codec.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/eval.c _testlimitedcapi/float.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/import.c _testlimitedcapi/list.c _testlimitedcapi/long.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/sys.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c _testlimitedcapi/version.c _testlimitedcapi/file.c
@MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c
@MODULE__TESTCLINIC_LIMITED_TRUE@_testclinic_limited _testclinic_limited.c
@MODULE__TEST_REACHABLE_TRUE@_test_reachable _test_reachable.c

# Some testing modules MUST be built as shared libraries.
*shared*
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
44 changes: 44 additions & 0 deletions Objects/codeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -1365,6 +1365,15 @@ lineiter_dealloc(PyObject *self)
Py_TYPE(li)->tp_free(li);
}

static int
lineiter_reachable(PyObject *self, visitproc visit, void *arg)
{
lineiterator *li = (lineiterator*)self;
Py_VISIT(Py_TYPE(self));
Py_VISIT(li->li_code);
return 0;
}

static PyObject *
_source_offset_converter(void *arg) {
int *value = (int*)arg;
Expand Down Expand Up @@ -1436,6 +1445,7 @@ PyTypeObject _PyLineIterator = {
0, /* tp_alloc */
0, /* tp_new */
PyObject_Free, /* tp_free */
.tp_reachable = lineiter_reachable,
};

static lineiterator *
Expand Down Expand Up @@ -1469,6 +1479,15 @@ positionsiter_dealloc(PyObject *self)
Py_TYPE(pi)->tp_free(pi);
}

static int
positionsiter_reachable(PyObject *self, visitproc visit, void *arg)
{
positionsiterator *pi = (positionsiterator*)self;
Py_VISIT(Py_TYPE(self));
Py_VISIT(pi->pi_code);
return 0;
}

static PyObject*
positionsiter_next(PyObject *self)
{
Expand Down Expand Up @@ -1529,6 +1548,7 @@ PyTypeObject _PyPositionsIterator = {
0, /* tp_alloc */
0, /* tp_new */
PyObject_Free, /* tp_free */
.tp_reachable = positionsiter_reachable,
};

static PyObject*
Expand Down Expand Up @@ -2465,6 +2485,29 @@ code_traverse(PyObject *self, visitproc visit, void *arg)
}
#endif

static int
code_reachable(PyObject *self, visitproc visit, void *arg)
{
PyCodeObject *co = _PyCodeObject_CAST(self);
Py_VISIT(Py_TYPE(self));
Py_VISIT(co->co_consts);
Py_VISIT(co->co_names);
Py_VISIT(co->co_exceptiontable);
Py_VISIT(co->co_localsplusnames);
Py_VISIT(co->co_localspluskinds);
Py_VISIT(co->co_filename);
Py_VISIT(co->co_name);
Py_VISIT(co->co_qualname);
Py_VISIT(co->co_linetable);
if (co->_co_cached != NULL) {
Py_VISIT(co->_co_cached->_co_code);
Py_VISIT(co->_co_cached->_co_varnames);
Py_VISIT(co->_co_cached->_co_cellvars);
Py_VISIT(co->_co_cached->_co_freevars);
}
Comment on lines +2502 to +2507
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes me worry about write barriers on the cache. There is a lot of stuff around modifying this for NoGIL

return 0;
}

static PyObject *
code_repr(PyObject *self)
{
Expand Down Expand Up @@ -2922,6 +2965,7 @@ PyTypeObject PyCode_Type = {
0, /* tp_init */
0, /* tp_alloc */
code_new, /* tp_new */
.tp_reachable = code_reachable,
};


Expand Down
Loading