Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add debug report dialog and helpers
Introduce a reusable debug text dialog and helper functions for generating and displaying DeepLabCut diagnostic reports. Adds deeplabcut/gui/dialogs/debug_dialog.py implementing DebugTextDialog (read-only log view with refresh/copy/keyboard shortcut), providers to render logs and full issue reports, show_debug_report_dialog to install/reuse recorders and present the report, and create_generate_debug_log_action to wire up a QAction. Also exports these symbols via deeplabcut/gui/dialogs/__init__.py. Dialog instances are attached to the parent to avoid GC and duplicate windows.
  • Loading branch information
C-Achard committed May 12, 2026
commit 85612c1866919f248151944eb4c41729f4a0993d
17 changes: 17 additions & 0 deletions deeplabcut/gui/dialogs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from collections.abc import Sequence

from .debug_dialog import (
DebugTextDialog,
create_generate_debug_log_action,
Comment thread
C-Achard marked this conversation as resolved.
make_issue_report_provider,
make_log_text_provider,
show_debug_report_dialog,
)

__all__: Sequence[str] = (
"DebugTextDialog",
"create_generate_debug_log_action",
"make_issue_report_provider",
"make_log_text_provider",
"show_debug_report_dialog",
)
296 changes: 296 additions & 0 deletions deeplabcut/gui/dialogs/debug_dialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
#
# DeepLabCut Toolbox (deeplabcut.org)
# © A. & M.W. Mathis Labs
# https://github.com/DeepLabCut/DeepLabCut
#
# Please see AUTHORS for contributors.
# https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS
#
# Licensed under GNU Lesser General Public License v3.0
#

from __future__ import annotations

from collections.abc import Callable, Iterable
from typing import TYPE_CHECKING

from PySide6 import QtGui
from PySide6.QtCore import Qt
from PySide6.QtGui import QAction, QFontDatabase, QKeySequence, QTextCursor
from PySide6.QtWidgets import (
QApplication,
QDialog,
QHBoxLayout,
QLabel,
QPlainTextEdit,
QPushButton,
QVBoxLayout,
QWidget,
)

from deeplabcut.core.debug import (
ExecutableSpec,
InMemoryDebugRecorder,
LibrarySpec,
build_debug_report,
get_debug_recorder,
install_debug_recorder,
)

if TYPE_CHECKING:
pass


def make_log_text_provider(
*,
recorder: InMemoryDebugRecorder | None,
limit: int = 300,
) -> Callable[[], str]:
"""Return a callable that renders recent captured logs."""

def _provider() -> str:
if recorder is None:
return "<debug recorder unavailable>"
return recorder.render_text(limit=limit)

return _provider


def make_issue_report_provider(
*,
recorder: InMemoryDebugRecorder | None,
libraries: Iterable[LibrarySpec | str] | None = None,
executables: Iterable[ExecutableSpec | str] | None = None,
include_module_paths: bool = False,
include_executable_paths: bool = True,
log_limit: int = 300,
) -> Callable[[], str]:
"""Return a callable that builds a full DLC debug report."""

def _provider() -> str:
return build_debug_report(
recorder=recorder,
libraries=libraries,
executables=executables,
Comment thread
C-Achard marked this conversation as resolved.
Outdated
include_module_paths=include_module_paths,
include_executable_paths=include_executable_paths,
log_limit=log_limit,
)

return _provider


class DebugTextDialog(QDialog):
"""
Minimal, application-agnostic debug text viewer.

This widget only knows how to:
- fetch text from a callable
- display it read-only
- copy it to clipboard
- refresh it on demand

It intentionally knows nothing about:
- recorder internals
- DLC main window internals
- environment/report formatting
"""

def __init__(
self,
*,
title: str,
text_provider: Callable[[], str],
parent: QWidget | None = None,
initial_hint: str = "Read-only diagnostic output",
) -> None:
super().__init__(parent=parent)
self.setWindowTitle(title)
self.setModal(False)
self.resize(950, 700)

self._text_provider = text_provider

self._build_ui(initial_hint=initial_hint)
self.refresh_text()

def _build_ui(self, *, initial_hint: str) -> None:
layout = QVBoxLayout(self)

self._hint_label = QLabel(initial_hint, self)
self._hint_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
layout.addWidget(self._hint_label)

self._text_edit = QPlainTextEdit(self)
self._text_edit.setReadOnly(True)
self._text_edit.setLineWrapMode(QPlainTextEdit.NoWrap)

# Use a fixed-width system font for logs / reports
font = QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont)
self._text_edit.setFont(font)

layout.addWidget(self._text_edit, stretch=1)

button_row = QHBoxLayout()

self._status_label = QLabel("", self)
self._status_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
button_row.addWidget(self._status_label, stretch=1)

self._refresh_btn = QPushButton("Refresh", self)
self._refresh_btn.clicked.connect(self.refresh_text)
button_row.addWidget(self._refresh_btn)

self._copy_btn = QPushButton("Copy to clipboard", self)
self._copy_btn.clicked.connect(self.copy_to_clipboard)
button_row.addWidget(self._copy_btn)

self._close_btn = QPushButton("Close", self)
self._close_btn.clicked.connect(self.close)
button_row.addWidget(self._close_btn)

layout.addLayout(button_row)

# Optional keyboard shortcut
copy_action = QAction(self)
copy_action.setShortcut(QKeySequence.StandardKey.Copy)
copy_action.triggered.connect(self.copy_to_clipboard)
self.addAction(copy_action)

def refresh_text(self) -> None:
try:
text = self._text_provider()
except Exception as exc:
text = f"[debug-dialog] failed to build debug text\n\n{exc!r}"

self._text_edit.setPlainText(text or "<no debug text available>")
Comment thread
C-Achard marked this conversation as resolved.
self._text_edit.moveCursor(QTextCursor.MoveOperation.Start)
self._status_label.setText("")

def copy_to_clipboard(self) -> None:
try:
text = self._text_edit.toPlainText()
QApplication.clipboard().setText(text)
self._status_label.setText("Copied to clipboard")
except Exception:
self._status_label.setText("Could not copy to clipboard")

def showEvent(self, event: QtGui.QShowEvent) -> None:
"""Refresh each time the dialog becomes visible."""
super().showEvent(event)
self.refresh_text()


def _get_or_create_debug_dialog(
*,
parent: QWidget,
title: str,
text_provider: Callable[[], str],
initial_hint: str,
attr_name: str = "_dlc_debug_dialog",
) -> DebugTextDialog:
"""
Reuse a single dialog instance attached to ``parent``.

Storing the dialog on the main window avoids accidental garbage collection
and prevents opening a pile of duplicate windows.
"""
dlg = getattr(parent, attr_name, None)
if isinstance(dlg, DebugTextDialog):
return dlg

dlg = DebugTextDialog(
title=title,
text_provider=text_provider,
parent=parent,
Comment thread
C-Achard marked this conversation as resolved.
initial_hint=initial_hint,
)
setattr(parent, attr_name, dlg)
return dlg


def show_debug_report_dialog(
*,
parent: QWidget,
recorder: InMemoryDebugRecorder | None = None,
logger_name: str = "deeplabcut",
libraries: Iterable[LibrarySpec | str] | None = None,
executables: Iterable[ExecutableSpec | str] | None = None,
include_module_paths: bool = False,
include_executable_paths: bool = True,
log_limit: int = 300,
dialog_attr_name: str = "_dlc_debug_dialog",
) -> DebugTextDialog:
"""
Open (or reuse) the full diagnostic report dialog.

If ``recorder`` is not provided, this function tries to reuse an existing
recorder for the given logger namespace and installs one if missing.
"""
if recorder is None:
recorder = get_debug_recorder(logger_name=logger_name)
if recorder is None:
recorder = install_debug_recorder(logger_name=logger_name)

provider = make_issue_report_provider(
recorder=recorder,
libraries=libraries,
executables=executables,
include_module_paths=include_module_paths,
include_executable_paths=include_executable_paths,
log_limit=log_limit,
)

dlg = _get_or_create_debug_dialog(
parent=parent,
title="DeepLabCut debug log",
text_provider=provider,
initial_hint=("Diagnostic report for issue reporting. Use Refresh to update, then Copy to clipboard."),
attr_name=dialog_attr_name,
)
dlg.refresh_text()
Comment thread
C-Achard marked this conversation as resolved.
Outdated
dlg.show()
dlg.raise_()
dlg.activateWindow()
return dlg


def create_generate_debug_log_action(
*,
parent: QWidget,
recorder: InMemoryDebugRecorder | None = None,
logger_name: str = "deeplabcut",
libraries: Iterable[LibrarySpec | str] | None = None,
executables: Iterable[ExecutableSpec | str] | None = None,
include_module_paths: bool = False,
include_executable_paths: bool = True,
log_limit: int = 300,
text: str = "&Generate debug log...",
status_tip: str = "Generate a diagnostic report for troubleshooting",
dialog_attr_name: str = "_dlc_debug_dialog",
) -> QAction:
"""
Create a QAction that opens the DLC debug report dialog.

Typical usage in ``MainWindow.create_actions``::

self.generateDebugLogAction = create_generate_debug_log_action(parent=self)
"""
action = QAction(text, parent)
action.setStatusTip(status_tip)

def _open_dialog() -> None:
show_debug_report_dialog(
parent=parent,
recorder=recorder,
logger_name=logger_name,
libraries=libraries,
executables=executables,
include_module_paths=include_module_paths,
include_executable_paths=include_executable_paths,
log_limit=log_limit,
dialog_attr_name=dialog_attr_name,
)

action.triggered.connect(_open_dialog)
return action