Skip to content

Commit 5900729

Browse files
Carl Meyerfacebook-github-bot
authored andcommitted
backport 3.12 type watchers
Summary: Backport python/cpython#97875 plus some later updates. The only change in the backport is that instead of adding `tp_watched` field to the `PyTypeObject` struct, we instead steal 8 bits of `tp_flags`. This is because some extension modules contain statically-defined `PyTypeObject` structs, and we don't want to have to recompile all extensions. If we don't, we'd get junk data from `type->tp_watched` for such statically defined type structs. There are no free bits in the lower 32 bits of `tp_flags`, but all the upper 32 bits are unused, since CPython supports 32-bit architectures. Since Cinder doesn't, we know those bits are present and free for our use, so we steal the lower 8 of them. Reviewed By: itamaro Differential Revision: D47202165 fbshipit-source-id: 370ac09e2bc7502742480cbbd0fb09804102f783
1 parent 9d5948d commit 5900729

File tree

6 files changed

+427
-1
lines changed

6 files changed

+427
-1
lines changed

CinderX/known-core-python-exported-symbols

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1764,10 +1764,12 @@ _PyTuple_Resize
17641764
PyTuple_SetItem
17651765
PyTuple_Size
17661766
PyTuple_Type
1767+
PyType_AddWatcher
17671768
_PyType_CalculateMetaclass
17681769
_PyType_CheckConsistency
17691770
PyType_ClearCache
17701771
_PyType_ClearNoShadowingInstances
1772+
PyType_ClearWatcher
17711773
PyType_FromModuleAndSpec
17721774
PyType_FromSpec
17731775
PyType_FromSpecWithBases
@@ -1789,6 +1791,8 @@ PyType_Ready
17891791
_PyType_SetNoShadowingInstances
17901792
_PyTypes_InitSlotDefs
17911793
PyType_Type
1794+
PyType_Unwatch
1795+
PyType_Watch
17921796
_Py_Uid_Converter
17931797
PyUnicode_Append
17941798
PyUnicode_AppendAndDel

Include/cpython/object.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,14 @@ PyAPI_FUNC(int) _PyTrash_cond(PyObject *op, destructor dealloc);
565565
#define Py_TRASHCAN_SAFE_BEGIN(op) Py_TRASHCAN_BEGIN_CONDITION(op, 1)
566566
#define Py_TRASHCAN_SAFE_END(op) Py_TRASHCAN_END
567567

568+
#define TYPE_MAX_WATCHERS 8
569+
570+
typedef int(*PyType_WatchCallback)(PyTypeObject *);
571+
PyAPI_FUNC(int) PyType_AddWatcher(PyType_WatchCallback callback);
572+
PyAPI_FUNC(int) PyType_ClearWatcher(int watcher_id);
573+
PyAPI_FUNC(int) PyType_Watch(int watcher_id, PyObject *type);
574+
PyAPI_FUNC(int) PyType_Unwatch(int watcher_id, PyObject *type);
575+
568576
/* Attempt to assign a version tag to the given type.
569577
*
570578
* Returns 1 if the type already had a valid version tag or a new one was

Include/internal/pycore_interp.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ struct _is {
298298
struct atexit_state atexit;
299299

300300
PyObject *audit_hooks;
301+
PyType_WatchCallback type_watchers[TYPE_MAX_WATCHERS];
301302
PyCode_WatchCallback code_watchers[CODE_MAX_WATCHERS];
302303
// One bit is set for each non-NULL entry in code_watchers
303304
uint8_t active_code_watchers;

Lib/test/test_capi.py

Lines changed: 180 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# these are all functions _testcapi exports whose name begins with 'test_'.
33

44
from collections import OrderedDict
5-
from contextlib import contextmanager
5+
from contextlib import contextmanager, ExitStack
66
import importlib.machinery
77
import importlib.util
88
import os
@@ -1221,6 +1221,185 @@ def test_clear_unassigned_watcher_id(self):
12211221
self.clear_watcher(1)
12221222

12231223

1224+
class TestTypeWatchers(unittest.TestCase):
1225+
# types of watchers testcapimodule can add:
1226+
TYPES = 0 # appends modified types to global event list
1227+
ERROR = 1 # unconditionally sets and signals a RuntimeException
1228+
WRAP = 2 # appends modified type wrapped in list to global event list
1229+
1230+
# duplicating the C constant
1231+
TYPE_MAX_WATCHERS = 8
1232+
1233+
def add_watcher(self, kind=TYPES):
1234+
return _testcapi.add_type_watcher(kind)
1235+
1236+
def clear_watcher(self, watcher_id):
1237+
_testcapi.clear_type_watcher(watcher_id)
1238+
1239+
@contextmanager
1240+
def watcher(self, kind=TYPES):
1241+
wid = self.add_watcher(kind)
1242+
try:
1243+
yield wid
1244+
finally:
1245+
self.clear_watcher(wid)
1246+
1247+
def assert_events(self, expected):
1248+
actual = _testcapi.get_type_modified_events()
1249+
self.assertEqual(actual, expected)
1250+
1251+
def watch(self, wid, t):
1252+
_testcapi.watch_type(wid, t)
1253+
1254+
def unwatch(self, wid, t):
1255+
_testcapi.unwatch_type(wid, t)
1256+
1257+
def test_watch_type(self):
1258+
class C: pass
1259+
with self.watcher() as wid:
1260+
self.watch(wid, C)
1261+
C.foo = "bar"
1262+
self.assert_events([C])
1263+
1264+
def test_event_aggregation(self):
1265+
class C: pass
1266+
with self.watcher() as wid:
1267+
self.watch(wid, C)
1268+
C.foo = "bar"
1269+
C.bar = "baz"
1270+
# only one event registered for both modifications
1271+
self.assert_events([C])
1272+
1273+
def test_lookup_resets_aggregation(self):
1274+
class C: pass
1275+
with self.watcher() as wid:
1276+
self.watch(wid, C)
1277+
C.foo = "bar"
1278+
# lookup resets type version tag
1279+
self.assertEqual(C.foo, "bar")
1280+
C.bar = "baz"
1281+
# both events registered
1282+
self.assert_events([C, C])
1283+
1284+
def test_unwatch_type(self):
1285+
class C: pass
1286+
with self.watcher() as wid:
1287+
self.watch(wid, C)
1288+
C.foo = "bar"
1289+
self.assertEqual(C.foo, "bar")
1290+
self.assert_events([C])
1291+
self.unwatch(wid, C)
1292+
C.bar = "baz"
1293+
self.assert_events([C])
1294+
1295+
def test_clear_watcher(self):
1296+
class C: pass
1297+
# outer watcher is unused, it's just to keep events list alive
1298+
with self.watcher() as _:
1299+
with self.watcher() as wid:
1300+
self.watch(wid, C)
1301+
C.foo = "bar"
1302+
self.assertEqual(C.foo, "bar")
1303+
self.assert_events([C])
1304+
C.bar = "baz"
1305+
# Watcher on C has been cleared, no new event
1306+
self.assert_events([C])
1307+
1308+
def test_watch_type_subclass(self):
1309+
class C: pass
1310+
class D(C): pass
1311+
with self.watcher() as wid:
1312+
self.watch(wid, D)
1313+
C.foo = "bar"
1314+
self.assert_events([D])
1315+
1316+
def test_error(self):
1317+
class C: pass
1318+
with self.watcher(kind=self.ERROR) as wid:
1319+
self.watch(wid, C)
1320+
with catch_unraisable_exception() as cm:
1321+
C.foo = "bar"
1322+
self.assertIs(cm.unraisable.object, C)
1323+
self.assertEqual(str(cm.unraisable.exc_value), "boom!")
1324+
self.assert_events([])
1325+
1326+
def test_two_watchers(self):
1327+
class C1: pass
1328+
class C2: pass
1329+
with self.watcher() as wid1:
1330+
with self.watcher(kind=self.WRAP) as wid2:
1331+
self.assertNotEqual(wid1, wid2)
1332+
self.watch(wid1, C1)
1333+
self.watch(wid2, C2)
1334+
C1.foo = "bar"
1335+
C2.hmm = "baz"
1336+
self.assert_events([C1, [C2]])
1337+
1338+
def test_all_watchers(self):
1339+
class C: pass
1340+
with ExitStack() as stack:
1341+
last_wid = -1
1342+
# don't make assumptions about how many watchers are already
1343+
# registered, just go until we reach the max ID
1344+
while last_wid < self.TYPE_MAX_WATCHERS - 1:
1345+
last_wid = stack.enter_context(self.watcher())
1346+
self.watch(last_wid, C)
1347+
C.foo = "bar"
1348+
self.assert_events([C])
1349+
1350+
def test_watch_non_type(self):
1351+
with self.watcher() as wid:
1352+
with self.assertRaisesRegex(ValueError, r"Cannot watch non-type"):
1353+
self.watch(wid, 1)
1354+
1355+
def test_watch_out_of_range_watcher_id(self):
1356+
class C: pass
1357+
with self.assertRaisesRegex(ValueError, r"Invalid type watcher ID -1"):
1358+
self.watch(-1, C)
1359+
with self.assertRaisesRegex(ValueError, r"Invalid type watcher ID 8"):
1360+
self.watch(self.TYPE_MAX_WATCHERS, C)
1361+
1362+
def test_watch_unassigned_watcher_id(self):
1363+
class C: pass
1364+
with self.assertRaisesRegex(ValueError, r"No type watcher set for ID 1"):
1365+
self.watch(1, C)
1366+
1367+
def test_unwatch_non_type(self):
1368+
with self.watcher() as wid:
1369+
with self.assertRaisesRegex(ValueError, r"Cannot watch non-type"):
1370+
self.unwatch(wid, 1)
1371+
1372+
def test_unwatch_out_of_range_watcher_id(self):
1373+
class C: pass
1374+
with self.assertRaisesRegex(ValueError, r"Invalid type watcher ID -1"):
1375+
self.unwatch(-1, C)
1376+
with self.assertRaisesRegex(ValueError, r"Invalid type watcher ID 8"):
1377+
self.unwatch(self.TYPE_MAX_WATCHERS, C)
1378+
1379+
def test_unwatch_unassigned_watcher_id(self):
1380+
class C: pass
1381+
with self.assertRaisesRegex(ValueError, r"No type watcher set for ID 1"):
1382+
self.unwatch(1, C)
1383+
1384+
def test_clear_out_of_range_watcher_id(self):
1385+
with self.assertRaisesRegex(ValueError, r"Invalid type watcher ID -1"):
1386+
self.clear_watcher(-1)
1387+
with self.assertRaisesRegex(ValueError, r"Invalid type watcher ID 8"):
1388+
self.clear_watcher(self.TYPE_MAX_WATCHERS)
1389+
1390+
def test_clear_unassigned_watcher_id(self):
1391+
with self.assertRaisesRegex(ValueError, r"No type watcher set for ID 1"):
1392+
self.clear_watcher(1)
1393+
1394+
def test_no_more_ids_available(self):
1395+
contexts = [self.watcher() for i in range(self.TYPE_MAX_WATCHERS)]
1396+
with ExitStack() as stack:
1397+
for ctx in contexts:
1398+
stack.enter_context(ctx)
1399+
with self.assertRaisesRegex(RuntimeError, r"no more type watcher IDs"):
1400+
self.add_watcher()
1401+
1402+
12241403
class TestCodeObjectWatchers(unittest.TestCase):
12251404
@contextmanager
12261405
def code_watcher(self, which_watcher):

Modules/_testcapimodule.c

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6265,6 +6265,128 @@ get_dict_watcher_events(PyObject *self, PyObject *Py_UNUSED(args))
62656265
return Py_NewRef(g_dict_watch_events);
62666266
}
62676267

6268+
// Test type watchers
6269+
static PyObject *g_type_modified_events;
6270+
static int g_type_watchers_installed;
6271+
6272+
static int
6273+
type_modified_callback(PyTypeObject *type)
6274+
{
6275+
assert(PyList_Check(g_type_modified_events));
6276+
if(PyList_Append(g_type_modified_events, (PyObject *)type) < 0) {
6277+
return -1;
6278+
}
6279+
return 0;
6280+
}
6281+
6282+
static int
6283+
type_modified_callback_wrap(PyTypeObject *type)
6284+
{
6285+
assert(PyList_Check(g_type_modified_events));
6286+
PyObject *list = PyList_New(0);
6287+
if (list == NULL) {
6288+
return -1;
6289+
}
6290+
if (PyList_Append(list, (PyObject *)type) < 0) {
6291+
Py_DECREF(list);
6292+
return -1;
6293+
}
6294+
if (PyList_Append(g_type_modified_events, list) < 0) {
6295+
Py_DECREF(list);
6296+
return -1;
6297+
}
6298+
Py_DECREF(list);
6299+
return 0;
6300+
}
6301+
6302+
static int
6303+
type_modified_callback_error(PyTypeObject *type)
6304+
{
6305+
PyErr_SetString(PyExc_RuntimeError, "boom!");
6306+
return -1;
6307+
}
6308+
6309+
static PyObject *
6310+
add_type_watcher(PyObject *self, PyObject *kind)
6311+
{
6312+
int watcher_id;
6313+
assert(PyLong_Check(kind));
6314+
long kind_l = PyLong_AsLong(kind);
6315+
if (kind_l == 2) {
6316+
watcher_id = PyType_AddWatcher(type_modified_callback_wrap);
6317+
}
6318+
else if (kind_l == 1) {
6319+
watcher_id = PyType_AddWatcher(type_modified_callback_error);
6320+
}
6321+
else {
6322+
watcher_id = PyType_AddWatcher(type_modified_callback);
6323+
}
6324+
if (watcher_id < 0) {
6325+
return NULL;
6326+
}
6327+
if (!g_type_watchers_installed) {
6328+
assert(!g_type_modified_events);
6329+
if (!(g_type_modified_events = PyList_New(0))) {
6330+
return NULL;
6331+
}
6332+
}
6333+
g_type_watchers_installed++;
6334+
return PyLong_FromLong(watcher_id);
6335+
}
6336+
6337+
static PyObject *
6338+
clear_type_watcher(PyObject *self, PyObject *watcher_id)
6339+
{
6340+
if (PyType_ClearWatcher(PyLong_AsLong(watcher_id))) {
6341+
return NULL;
6342+
}
6343+
g_type_watchers_installed--;
6344+
if (!g_type_watchers_installed) {
6345+
assert(g_type_modified_events);
6346+
Py_CLEAR(g_type_modified_events);
6347+
}
6348+
Py_RETURN_NONE;
6349+
}
6350+
6351+
static PyObject *
6352+
get_type_modified_events(PyObject *self, PyObject *Py_UNUSED(args))
6353+
{
6354+
if (!g_type_modified_events) {
6355+
PyErr_SetString(PyExc_RuntimeError, "no watchers active");
6356+
return NULL;
6357+
}
6358+
return Py_NewRef(g_type_modified_events);
6359+
}
6360+
6361+
static PyObject *
6362+
watch_type(PyObject *module, PyObject *args)
6363+
{
6364+
int watcher_id;
6365+
PyObject *type;
6366+
if (!PyArg_ParseTuple(args, "iO", &watcher_id, &type)) {
6367+
return NULL;
6368+
}
6369+
if (PyType_Watch(watcher_id, type)) {
6370+
return NULL;
6371+
}
6372+
Py_RETURN_NONE;
6373+
}
6374+
6375+
static PyObject *
6376+
unwatch_type(PyObject *module, PyObject *args)
6377+
{
6378+
int watcher_id;
6379+
PyObject *type;
6380+
if (!PyArg_ParseTuple(args, "iO", &watcher_id, &type)) {
6381+
return NULL;
6382+
}
6383+
if (PyType_Unwatch(watcher_id, type)) {
6384+
return NULL;
6385+
}
6386+
Py_RETURN_NONE;
6387+
}
6388+
6389+
62686390
// Test code object watching
62696391

62706392
#define NUM_CODE_WATCHERS 2
@@ -6925,6 +7047,11 @@ static PyMethodDef TestMethods[] = {
69257047
{"watch_dict", watch_dict, METH_VARARGS},
69267048
{"unwatch_dict", unwatch_dict, METH_VARARGS},
69277049
{"get_dict_watcher_events", get_dict_watcher_events, METH_NOARGS},
7050+
{"add_type_watcher", add_type_watcher, METH_O},
7051+
{"clear_type_watcher", clear_type_watcher, METH_O},
7052+
{"watch_type", watch_type, METH_VARARGS},
7053+
{"unwatch_type", unwatch_type, METH_VARARGS},
7054+
{"get_type_modified_events", get_type_modified_events, METH_NOARGS},
69287055
{"add_code_watcher", add_code_watcher, METH_O},
69297056
{"clear_code_watcher", clear_code_watcher, METH_O},
69307057
{"get_code_watcher_num_created_events",

0 commit comments

Comments
 (0)