Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 33.9k
gh-91051: allow setting a callback hook on PyType_Modified#97875
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
f2b70a21a28a4595e49ee1af2a98415ed4929e38df0de5aff27a4980d67d6fdcb71ae9ab1606c26ddee24783e0771d48c121f42ee6e535142b42bfecdbe93b43297438bba5da22d190bFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -587,6 +587,12 @@ New Features | ||
| :c:func:`PyDict_AddWatch` and related APIs to be called whenever a dictionary | ||
| is modified. This is intended for use by optimizing interpreters, JIT | ||
| compilers, or debuggers. | ||
| (Contributed by Carl Meyer in :gh:`91052`.) | ||
| * Added :c:func:`PyType_AddWatcher` and :c:func:`PyType_Watch` API to register | ||
carljm marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading. Please reload this page. | ||
| callbacks to receive notification on changes to a type. | ||
| (Contributed by Carl Meyer in :gh:`91051`.) | ||
| Porting to Python 3.12 | ||
| ---------------------- | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -2,7 +2,7 @@ | ||
| # these are all functions _testcapi exports whose name begins with 'test_'. | ||
| from collections import OrderedDict | ||
| from contextlib import contextmanager | ||
| from contextlib import contextmanager, ExitStack | ||
| import _thread | ||
| import importlib.machinery | ||
| import importlib.util | ||
| @@ -1606,5 +1606,172 @@ def test_clear_unassigned_watcher_id(self): | ||
| self.clear_watcher(1) | ||
| class TestTypeWatchers(unittest.TestCase): | ||
| # types of watchers testcapimodule can add: | ||
| TYPES = 0 # appends modified types to global event list | ||
| ERROR = 1 # unconditionally sets and signals a RuntimeException | ||
| WRAP = 2 # appends modified type wrapped in list to global event list | ||
| # duplicating the C constant | ||
| TYPE_MAX_WATCHERS = 8 | ||
| def add_watcher(self, kind=TYPES): | ||
| return _testcapi.add_type_watcher(kind) | ||
| def clear_watcher(self, watcher_id): | ||
| _testcapi.clear_type_watcher(watcher_id) | ||
| @contextmanager | ||
| def watcher(self, kind=TYPES): | ||
| wid = self.add_watcher(kind) | ||
| try: | ||
| yield wid | ||
| finally: | ||
| self.clear_watcher(wid) | ||
| def assert_events(self, expected): | ||
| actual = _testcapi.get_type_modified_events() | ||
| self.assertEqual(actual, expected) | ||
| def watch(self, wid, t): | ||
| _testcapi.watch_type(wid, t) | ||
| def unwatch(self, wid, t): | ||
| _testcapi.unwatch_type(wid, t) | ||
| def test_watch_type(self): | ||
| class C: pass | ||
| with self.watcher() as wid: | ||
| self.watch(wid, C) | ||
| C.foo = "bar" | ||
| self.assert_events([C]) | ||
| def test_event_aggregation(self): | ||
| class C: pass | ||
| with self.watcher() as wid: | ||
| self.watch(wid, C) | ||
| C.foo = "bar" | ||
| C.bar = "baz" | ||
| # only one event registered for both modifications | ||
| self.assert_events([C]) | ||
| def test_lookup_resets_aggregation(self): | ||
| class C: pass | ||
| with self.watcher() as wid: | ||
| self.watch(wid, C) | ||
| C.foo = "bar" | ||
| # lookup resets type version tag | ||
| self.assertEqual(C.foo, "bar") | ||
| C.bar = "baz" | ||
| # both events registered | ||
| self.assert_events([C, C]) | ||
| def test_unwatch_type(self): | ||
carljm marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading. Please reload this page. | ||
| class C: pass | ||
| with self.watcher() as wid: | ||
| self.watch(wid, C) | ||
| C.foo = "bar" | ||
| self.assertEqual(C.foo, "bar") | ||
| self.assert_events([C]) | ||
| self.unwatch(wid, C) | ||
| C.bar = "baz" | ||
| self.assert_events([C]) | ||
| def test_clear_watcher(self): | ||
| class C: pass | ||
| # outer watcher is unused, it's just to keep events list alive | ||
| with self.watcher() as _: | ||
| with self.watcher() as wid: | ||
| self.watch(wid, C) | ||
| C.foo = "bar" | ||
| self.assertEqual(C.foo, "bar") | ||
| self.assert_events([C]) | ||
| C.bar = "baz" | ||
| # Watcher on C has been cleared, no new event | ||
| self.assert_events([C]) | ||
| def test_watch_type_subclass(self): | ||
| class C: pass | ||
| class D(C): pass | ||
| with self.watcher() as wid: | ||
| self.watch(wid, D) | ||
| C.foo = "bar" | ||
| self.assert_events([D]) | ||
| def test_error(self): | ||
| class C: pass | ||
| with self.watcher(kind=self.ERROR) as wid: | ||
| self.watch(wid, C) | ||
| with catch_unraisable_exception() as cm: | ||
| C.foo = "bar" | ||
| self.assertIs(cm.unraisable.object, C) | ||
| self.assertEqual(str(cm.unraisable.exc_value), "boom!") | ||
| self.assert_events([]) | ||
| def test_two_watchers(self): | ||
| class C1: pass | ||
| class C2: pass | ||
| with self.watcher() as wid1: | ||
| with self.watcher(kind=self.WRAP) as wid2: | ||
| self.assertNotEqual(wid1, wid2) | ||
| self.watch(wid1, C1) | ||
carljm marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading. Please reload this page. | ||
| self.watch(wid2, C2) | ||
| C1.foo = "bar" | ||
| C2.hmm = "baz" | ||
| self.assert_events([C1, [C2]]) | ||
| def test_watch_non_type(self): | ||
| with self.watcher() as wid: | ||
| with self.assertRaisesRegex(ValueError, r"Cannot watch non-type"): | ||
| self.watch(wid, 1) | ||
| def test_watch_out_of_range_watcher_id(self): | ||
| class C: pass | ||
| with self.assertRaisesRegex(ValueError, r"Invalid type watcher ID -1"): | ||
| self.watch(-1, C) | ||
| with self.assertRaisesRegex(ValueError, r"Invalid type watcher ID 8"): | ||
| self.watch(self.TYPE_MAX_WATCHERS, C) | ||
| def test_watch_unassigned_watcher_id(self): | ||
| class C: pass | ||
| with self.assertRaisesRegex(ValueError, r"No type watcher set for ID 1"): | ||
| self.watch(1, C) | ||
| def test_unwatch_non_type(self): | ||
| with self.watcher() as wid: | ||
| with self.assertRaisesRegex(ValueError, r"Cannot watch non-type"): | ||
| self.unwatch(wid, 1) | ||
| def test_unwatch_out_of_range_watcher_id(self): | ||
| class C: pass | ||
| with self.assertRaisesRegex(ValueError, r"Invalid type watcher ID -1"): | ||
| self.unwatch(-1, C) | ||
| with self.assertRaisesRegex(ValueError, r"Invalid type watcher ID 8"): | ||
| self.unwatch(self.TYPE_MAX_WATCHERS, C) | ||
| def test_unwatch_unassigned_watcher_id(self): | ||
| class C: pass | ||
| with self.assertRaisesRegex(ValueError, r"No type watcher set for ID 1"): | ||
| self.unwatch(1, C) | ||
| def test_clear_out_of_range_watcher_id(self): | ||
| with self.assertRaisesRegex(ValueError, r"Invalid type watcher ID -1"): | ||
| self.clear_watcher(-1) | ||
| with self.assertRaisesRegex(ValueError, r"Invalid type watcher ID 8"): | ||
| self.clear_watcher(self.TYPE_MAX_WATCHERS) | ||
| def test_clear_unassigned_watcher_id(self): | ||
| with self.assertRaisesRegex(ValueError, r"No type watcher set for ID 1"): | ||
| self.clear_watcher(1) | ||
| def test_no_more_ids_available(self): | ||
| contexts = [self.watcher() for i in range(self.TYPE_MAX_WATCHERS)] | ||
| with ExitStack() as stack: | ||
| for ctx in contexts: | ||
| stack.enter_context(ctx) | ||
| with self.assertRaisesRegex(RuntimeError, r"no more type watcher IDs"): | ||
| self.add_watcher() | ||
| if __name__ == "__main__": | ||
| unittest.main() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| Add :c:func:`PyType_Watch` and related APIs to allow callbacks on | ||
| :c:func:`PyType_Modified`. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.