Confusing behavior with `sys.monitoring.DISABLE`
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 77.2k
- Forks
- 36k
- PR merge metrics
- PR metrics pending
Description
Bug report
Bug description:
# Adapted from https://github.com/python/cpython/blob/a5eeb832c2bbbd6ce1e9d545a553de926af468d5/Lib/test/test_monitoring.py#L670-L682
import sys
TEST_TOOL = 2
E = sys.monitoring.events
INSTRUMENTED_EVENTS = [
(E.PY_START, "start"),
# (E.PY_RETURN, "return"), # Uncomment this line leads to different behavior
]
class CounterWithDisable:
def __init__(self):
self.disable = False
self.count = 0
def __call__(self, *args):
print("cb", self)
self.count += 1
if self.disable:
return sys.monitoring.DISABLE
def foo(x):
return x + 1
def call_foo(x):
yield 2 * foo(x + 5)
def test():
sys.monitoring.use_tool_id(TEST_TOOL, "test")
for event, name in INSTRUMENTED_EVENTS:
print("Event", name)
try:
counter = CounterWithDisable()
counter.disable = True
sys.monitoring.register_callback(TEST_TOOL, event, counter)
sys.monitoring.set_events(TEST_TOOL, event)
list(call_foo(1))
print("counter.count", counter.count)
assert (counter.count < 4)
finally:
sys.monitoring.set_events(TEST_TOOL, 0)
sys.monitoring.register_callback(TEST_TOOL, event, None)
sys.monitoring.free_tool_id(TEST_TOOL)
print("First run".center(80, '='))
test()
print("Second run".center(80, '='))
test()
The above script prints:
===================================First run====================================
Event start
cb <__main__.CounterWithDisable object at 0x10944c0b0>
cb <__main__.CounterWithDisable object at 0x10944c0b0>
counter.count 2
===================================Second run===================================
Event start
counter.count 0
The first run "leaks" the disabled callback to the second run.
If I uncomment the line:
# (E.PY_RETURN, "return"), # Uncomment this line leads to different behavior
The script will print:
===================================First run====================================
Event start
cb <__main__.CounterWithDisable object at 0x10e34c1d0>
cb <__main__.CounterWithDisable object at 0x10e34c1d0>
counter.count 2
Event return
cb <__main__.CounterWithDisable object at 0x10e34c200>
cb <__main__.CounterWithDisable object at 0x10e34c200>
counter.count 2
===================================Second run===================================
Event start
cb <__main__.CounterWithDisable object at 0x10e34c2f0>
cb <__main__.CounterWithDisable object at 0x10e34c2f0>
counter.count 2
Event return
cb <__main__.CounterWithDisable object at 0x10e34c1d0>
cb <__main__.CounterWithDisable object at 0x10e34c1d0>
counter.count 2
It stops the "leaking".
This problem can be workarounded by calling sys.monitoring.restart_events() at the end of test(). But, it is not clear if it is necessary. This cpython test: https://github.com/python/cpython/blob/a5eeb832c2bbbd6ce1e9d545a553de926af468d5/Lib/test/test_monitoring.py#L670-L682
does not use sys.monitoring.restart_events().
CPython versions tested on:
3.12
Operating systems tested on:
Linux, macOS
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by running the reproducer on CPython 3.12 and compare its behavior with Lib/test/test_monitoring.py at the referenced lines. Trace how sys.monitoring.DISABLE persists across test() runs and how restart_events() changes that behavior. Done means the intended callback lifecycle is established, covered by a regression test, and the confusing reuse is resolved or documented.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- devtools
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100