[Python] Connect Python callables to signals directly, no TPyDispatcher - #23111
Open
guitargeek wants to merge 3 commits into
Open
[Python] Connect Python callables to signals directly, no TPyDispatcher#23111guitargeek wants to merge 3 commits into
guitargeek wants to merge 3 commits into
Conversation
TQObject::Connect() now directly accepts a Python callable as the slot,
e.g. button.Connect("Clicked()", on_clicked). The pythonization creates
the dispatcher internally and keeps it alive for the lifetime of the
connection, which can be undone with Disconnect(signal, callable). The
signal arguments are forwarded to the callable, as far as its signature
accepts them.
The dispatcher is a minimal class generated in the interpreter for
exactly the signal's argument types, holding the callable as a
std::function. The conversion from the Python callable and the callback
into Python are done by cppyy, with the compiled entry point captured by
address inside the std::function. Consequently:
* The pythonization layer no longer installs a public C++ header that
interacts with the Python C API (the PyObject forward declaration in
ROOT/TPyDispatcher.h).
* Signals of any signature can be connected, no longer only those
covered by the fixed set of TPyDispatcher::Dispatch() overloads.
* The interpreter does not need to resolve any symbols from
libROOTPythonizations. That requirement is what had silently broken
TPyDispatcher in recent releases: the library is loaded as a Python
extension module without exposing its symbols for linking, so
jitting calls into it failed with unresolved symbols.
The TPyDispatcher class, its sources, and the ROOT.TPyDispatcher
attribute are removed. Migration: replace
obj.Connect(signal, "TPyDispatcher", disp, "Dispatch()") with
obj.Connect(signal, callable).
The GUI tutorials are updated accordingly. ModelInspector.py in
particular relied on use-after-free before: its dispatchers were method
locals that were garbage collected right after Connect() returned.
🤖 Done with the help of AI
The Python version of this tutorial had two bugs that went unnoticed
because its GUI callbacks never actually fired (signal connections
through TPyDispatcher silently failed to resolve their symbols) and the
tutorial is excluded from CI:
* The per-parameter widgets (frame, label, slider) were only stored in
C++ containers holding raw pointers, so Python garbage-collected the
proxies on each loop iteration and deleted the C++ widgets under the
parent frame, crashing e.g. in MapSubwindows().
* The sliders were connected to DoSlider before their positions were
initialized. Setting the position emits the connected signals, so
DoSlider ran on the half-constructed GUI and crashed.
Verified under Xvfb: the tutorial now runs to completion, and clicking
the fit button and checkboxes programmatically runs DoFit/HandleButtons
including the final DoSlider update.
🤖 Done with the help of AI
guitargeek
force-pushed
the
pyroot-connect-callables
branch
from
August 19, 2026 17:27
982bcc6 to
8fcd8cd
Compare
Test Results 23 files 23 suites 3d 16h 55m 59s ⏱️ For more details on these failures, see this check. Results for commit 8fcd8cdb. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TQObject::Connect() now directly accepts a Python callable as the slot, e.g. button.Connect("Clicked()", on_clicked). The pythonization creates the dispatcher internally and keeps it alive for the lifetime of the connection, which can be undone with Disconnect(signal, callable). The signal arguments are forwarded to the callable, as far as its signature accepts them.
The dispatcher is a minimal class generated in the interpreter for exactly the signal's argument types, holding the callable as a std::function. The conversion from the Python callable and the callback into Python are done by cppyy, with the compiled entry point captured by address inside the std::function. Consequently:
The pythonization layer no longer installs a public C++ header that interacts with the Python C API (the PyObject forward declaration in ROOT/TPyDispatcher.h).
Signals of any signature can be connected, no longer only those covered by the fixed set of TPyDispatcher::Dispatch() overloads.
The interpreter does not need to resolve any symbols from libROOTPythonizations. That requirement is what had silently broken TPyDispatcher in recent releases: the library is loaded as a Python extension module without exposing its symbols for linking, so jitting calls into it failed with unresolved symbols.
The TPyDispatcher class, its sources, and the ROOT.TPyDispatcher attribute are removed. Migration: replace
obj.Connect(signal, "TPyDispatcher", disp, "Dispatch()")withobj.Connect(signal, callable).The GUI tutorials are updated accordingly. ModelInspector.py in particular relied on use-after-free before: its dispatchers were method locals that were garbage collected right after Connect() returned.
🤖 Done with the help of AI