|
2 | 2 | # these are all functions _testcapi exports whose name begins with 'test_'. |
3 | 3 |
|
4 | 4 | from collections import OrderedDict |
5 | | -from contextlib import contextmanager |
| 5 | +from contextlib import contextmanager, ExitStack |
6 | 6 | import importlib.machinery |
7 | 7 | import importlib.util |
8 | 8 | import os |
@@ -1221,6 +1221,185 @@ def test_clear_unassigned_watcher_id(self): |
1221 | 1221 | self.clear_watcher(1) |
1222 | 1222 |
|
1223 | 1223 |
|
| 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 | + |
1224 | 1403 | class TestCodeObjectWatchers(unittest.TestCase): |
1225 | 1404 | @contextmanager |
1226 | 1405 | def code_watcher(self, which_watcher): |
|
0 commit comments