Skip to content

[Python] Connect Python callables to signals directly, no TPyDispatcher - #23111

Open
guitargeek wants to merge 3 commits into
root-project:masterfrom
guitargeek:pyroot-connect-callables
Open

[Python] Connect Python callables to signals directly, no TPyDispatcher#23111
guitargeek wants to merge 3 commits into
root-project:masterfrom
guitargeek:pyroot-connect-callables

Conversation

@guitargeek

Copy link
Copy Markdown
Contributor

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

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
guitargeek force-pushed the pyroot-connect-callables branch from 982bcc6 to 8fcd8cd Compare August 19, 2026 17:27
@github-actions

Copy link
Copy Markdown

Test Results

    23 files      23 suites   3d 16h 55m 59s ⏱️
 3 861 tests  3 859 ✅ 0 💤 2 ❌
79 562 runs  79 560 ✅ 0 💤 2 ❌

For more details on these failures, see this check.

Results for commit 8fcd8cdb.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant