From dc059595aa1a7f7e199a581f0a84c1ab85ca8924 Mon Sep 17 00:00:00 2001 From: goanpeca Date: Tue, 14 Apr 2020 11:43:07 -0500 Subject: [PATCH] Migrate breakpoint plugin to new API --- spyder/app/mainwindow.py | 10 +- spyder/plugins/breakpoints/api.py | 16 + spyder/plugins/breakpoints/plugin.py | 142 ++++--- .../breakpoints/widgets/breakpointsgui.py | 354 ++++++++++++------ 4 files changed, 338 insertions(+), 184 deletions(-) create mode 100644 spyder/plugins/breakpoints/api.py diff --git a/spyder/app/mainwindow.py b/spyder/app/mainwindow.py index 7782db00551..beb8a6202c0 100644 --- a/spyder/app/mainwindow.py +++ b/spyder/app/mainwindow.py @@ -1212,7 +1212,15 @@ def create_edit_action(text, tr_text, icon): # Load other plugins (former external plugins) # TODO: Use this bucle to load all internall plugins and remove # duplicated code - other_plugins = ['breakpoints', 'profiler', 'pylint'] + + # Breakpoints + if CONF.get('breakpoints', 'enable'): + from spyder.plugins.breakpoints.plugin import Breakpoints + self.breakpoints = Breakpoints(self, configuration=CONF) + self.register_plugin(self.breakpoints) + self.thirdparty_plugins.append(self.breakpoints) + + other_plugins = ['profiler', 'pylint'] for plugin_name in other_plugins: if CONF.get(plugin_name, 'enable'): module = importlib.import_module( diff --git a/spyder/plugins/breakpoints/api.py b/spyder/plugins/breakpoints/api.py new file mode 100644 index 00000000000..becf6edd032 --- /dev/null +++ b/spyder/plugins/breakpoints/api.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2009- Spyder Project Contributors +# +# Distributed under the terms of the MIT License +# (see spyder/__init__.py for details) + +# Standard library imports +from enum import Enum + +# Local imports +from spyder.plugins.breakpoints.widgets.breakpointsgui import ( + BreakpointTableActions) + + +class PluginActions(Enum): + ListBreakpoints = 'list_breakpoints' diff --git a/spyder/plugins/breakpoints/plugin.py b/spyder/plugins/breakpoints/plugin.py index 47a97b77440..0647d1ee2b5 100644 --- a/spyder/plugins/breakpoints/plugin.py +++ b/spyder/plugins/breakpoints/plugin.py @@ -19,78 +19,96 @@ import os.path as osp # Third party imports +from qtpy.QtCore import Signal from qtpy.QtWidgets import QVBoxLayout # Local imports -from spyder.config.base import _ -from spyder.utils import icon_manager as ima -from spyder.utils.qthelpers import create_action -from spyder.api.plugins import SpyderPluginWidget +from spyder.api.translations import get_translation +from spyder.api.plugins import Plugins, SpyderDockablePlugin, ApplicationMenus +from spyder.plugins.breakpoints.api import PluginActions +from spyder.plugins.breakpoints.widgets.breakpointsgui import ( + BreakpointWidget) -from .widgets.breakpointsgui import BreakpointWidget +_ = get_translation('spyder') -class Breakpoints(SpyderPluginWidget): - """Breakpoint list""" - CONF_SECTION = 'breakpoints' - CONF_FILE = False - - def __init__(self, parent=None): - """Initialization.""" - super().__init__(parent) - - self.breakpoints = BreakpointWidget(self, - options_button=self.options_button) - - layout = QVBoxLayout() - layout.addWidget(self.breakpoints) - self.setLayout(layout) - - self.breakpoints.set_data() - path = osp.join(self.PLUGIN_PATH, self.IMG_PATH) - self.icon = ima.icon('breakpoints', icon_path=path) +class Breakpoints(SpyderDockablePlugin): + """ + Breakpoint list Plugin. + """ - # ----- SpyderPluginWidget API -------------------------------------------- - def get_plugin_title(self): - """Return widget title""" + NAME = 'breakpoints' + CONF_SECTION = NAME + CONF_FILE = False + REQUIRES = [Plugins.Editor] + TABIFY = Plugins.Help + WIDGET_CLASS = BreakpointWidget + + # Signals + sig_clear_all_breakpoints_requested = Signal() + sig_clear_breakpoint_requested = Signal(str, int) + sig_edit_goto_requested = Signal(str, int, str) + sig_set_or_edit_conditional_breakpoint_requested = Signal() + + # --- SpyderDockablePlugin API + # ------------------------------------------------------------------------ + def get_name(self): return _("Breakpoints") - def get_plugin_icon(self): - """Return widget icon""" - return self.icon + def get_icon(self): + path = osp.join(self.get_path(), self.IMG_PATH) + return self.create_icon('breakpoints', path=path) + + def register(self): + widget = self.get_widget() + editor = self.get_plugin(Plugins.Editor) + + # TODO: change name of this signal + editor.breakpoints_saved.connect(self.set_data) + widget.sig_clear_all_breakpoints_requested.connect( + editor.clear_all_breakpoints) + widget.sig_clear_breakpoint_requested.connect(editor.clear_breakpoint) + widget.sig_edit_goto_requested.connect(editor.load) + widget.sig_set_or_edit_conditional_breakpoint_requested.connect( + editor.set_or_edit_conditional_breakpoint) + + widget.sig_clear_all_breakpoints_requested.connect( + self.sig_clear_all_breakpoints_requested) + widget.sig_clear_breakpoint_requested.connect( + self.sig_clear_breakpoint_requested) + widget.sig_edit_goto_requested.connect(self.sig_edit_goto_requested) + widget.sig_set_or_edit_conditional_breakpoint_requested.connect( + self.sig_set_or_edit_conditional_breakpoint_requested) + + list_action = self.create_action( + PluginActions.ListBreakpoints, + _("List breakpoints"), + triggered=lambda: self.switch_to_plugin(), + icon=self.get_icon(), + ) + + # TODO: Fix location once the sections are defined + debug_menu = self.get_application_menu(ApplicationMenus.Debug) + self.add_item_to_application_menu(list_action, debug_menu) + editor.pythonfile_dependent_actions += [list_action] + + # --- API + # ------------------------------------------------------------------------ + def set_data(self): + """ + Set breakpoint data on widget. + """ + self.get_widget().set_data(self.load_data()) - def get_focus_widget(self): + def load_data(self): """ - Return the widget to give focus to when - this plugin's dockwidget is raised on top-level + Load breakpoint data from configuration file. """ - return self.breakpoints.dictwidget - - def on_first_registration(self): - """Action to be performed on first plugin registration""" - self.tabify(self.main.help) - - def register_plugin(self): - """Register plugin in Spyder's main window""" - self.breakpoints.edit_goto.connect(self.main.editor.load) - self.breakpoints.clear_all_breakpoints.connect( - self.main.editor.clear_all_breakpoints) - self.breakpoints.clear_breakpoint.connect( - self.main.editor.clear_breakpoint) - self.main.editor.breakpoints_saved.connect(self.breakpoints.set_data) - self.breakpoints.set_or_edit_conditional_breakpoint.connect( - self.main.editor.set_or_edit_conditional_breakpoint) - - self.add_dockwidget() - - list_action = create_action(self, _("List breakpoints"), - triggered=self.show, icon=self.icon) - list_action.setEnabled(True) - pos = self.main.debug_menu_actions.index('list_breakpoints') - self.main.debug_menu_actions.insert(pos, list_action) - self.main.editor.pythonfile_dependent_actions += [list_action] - - def show(self): - """Show the breakpoints dockwidget""" - self.switch_to_plugin() + breakpoints_dict = self.get_conf_option('breakpoints', default={}, + section='run') + for filename in list(breakpoints_dict.keys()): + if not osp.isfile(filename): + breakpoints_dict.pop(filename) + + return breakpoints_dict diff --git a/spyder/plugins/breakpoints/widgets/breakpointsgui.py b/spyder/plugins/breakpoints/widgets/breakpointsgui.py index 8e0b227f669..cd307577ea7 100644 --- a/spyder/plugins/breakpoints/widgets/breakpointsgui.py +++ b/spyder/plugins/breakpoints/widgets/breakpointsgui.py @@ -5,7 +5,9 @@ # Licensed under the terms of the MIT License # (see spyder/__init__.py for details) -"""Breakpoint widget""" +""" +Breakpoint widget. +""" # pylint: disable=C0103 # pylint: disable=R0903 @@ -13,30 +15,27 @@ # pylint: disable=R0201 # Standard library imports -import os.path as osp +from enum import Enum import sys # Third party imports from qtpy import API from qtpy.compat import to_qvariant from qtpy.QtCore import QAbstractTableModel, QModelIndex, Qt, Signal -from qtpy.QtWidgets import (QItemDelegate, QMenu, QTableView, QHBoxLayout, - QVBoxLayout, QWidget) +from qtpy.QtWidgets import QItemDelegate, QTableView, QVBoxLayout # Local imports -from spyder.config.base import get_translation -from spyder.config.manager import CONF -from spyder.utils.qthelpers import (add_actions, create_action, - create_plugin_layout) +from spyder.api.translations import get_translation +from spyder.api.widgets import (MainWidgetMenus, PluginMainWidget, + SpyderWidgetMixin) from spyder.utils.sourcecode import disambiguate_fname -# This is needed for testing this module as a stand alone script -try: - _ = get_translation("breakpoints", "spyder_breakpoints") -except KeyError: - import gettext - _ = gettext.gettext +# Localization +_ = get_translation('spyder') + + +# Constants COLUMN_COUNT = 4 EXTRA_COLUMNS = 1 COL_FILE, COL_LINE, COL_CONDITION, COL_BLANK, COL_FULL = list( @@ -44,21 +43,33 @@ COLUMN_HEADERS = (_("File"), _("Line"), _("Condition"), ("")) +# --- Enums +# ---------------------------------------------------------------------------- +class BreakpointTableActions(Enum): + ClearAllBreakpoints = 'clear_all_breakpoints' + ClearBreakpoint = 'clear_breakpoint' + EditBreakpoint = 'edit_breakpoint' + + +# --- Widgets +# ---------------------------------------------------------------------------- class BreakpointTableModel(QAbstractTableModel): """ - Table model for breakpoints dictionary + Table model for breakpoints dictionary. """ def __init__(self, parent, data): super().__init__(parent) - if data is None: - data = {} - self._data = None + + self._data = {} if data is None else data self.breakpoints = None - self.set_data(data) + + self.set_data(self._data) def set_data(self, data): - """Set model data""" + """ + Set model data. + """ self._data = data self.breakpoints = [] files = [] @@ -66,6 +77,7 @@ def set_data(self, data): for key in data: if data[key] and key not in files: files.append(key) + # Insert items for key in files: for item in data[key]: @@ -75,15 +87,21 @@ def set_data(self, data): self.reset() def rowCount(self, qindex=QModelIndex()): - """Array row number""" + """ + Array row number. + """ return len(self.breakpoints) def columnCount(self, qindex=QModelIndex()): - """Array column count""" + """ + Array column count. + """ return COLUMN_COUNT def sort(self, column, order=Qt.DescendingOrder): - """Overriding sort method""" + """ + Overriding sort method. + """ if column == COL_FILE: self.breakpoints.sort(key=lambda breakp: int(breakp[COL_LINE])) self.breakpoints.sort(key=lambda breakp: breakp[COL_FILE]) @@ -93,12 +111,16 @@ def sort(self, column, order=Qt.DescendingOrder): pass elif column == COL_BLANK: pass + self.reset() def headerData(self, section, orientation, role=Qt.DisplayRole): - """Overriding method headerData""" + """ + Overriding method headerData. + """ if role != Qt.DisplayRole: return to_qvariant() + i_column = int(section) if orientation == Qt.Horizontal: return to_qvariant(COLUMN_HEADERS[i_column]) @@ -106,13 +128,18 @@ def headerData(self, section, orientation, role=Qt.DisplayRole): return to_qvariant() def get_value(self, index): - """Return current value""" + """ + Return current value. + """ return self.breakpoints[index.row()][index.column()] def data(self, index, role=Qt.DisplayRole): - """Return data at table index""" + """ + Return data at table index. + """ if not index.isValid(): return to_qvariant() + if role == Qt.DisplayRole: value = self.get_value(index) return to_qvariant(value) @@ -136,134 +163,219 @@ def reset(self): class BreakpointDelegate(QItemDelegate): + def __init__(self, parent=None): super().__init__(parent) -class BreakpointTableView(QTableView): - edit_goto = Signal(str, int, str) - clear_breakpoint = Signal(str, int) - clear_all_breakpoints = Signal() - set_or_edit_conditional_breakpoint = Signal() +class BreakpointTableView(QTableView, SpyderWidgetMixin): + """ + Table to display editor code breakpoints. + """ + + # Signals + sig_clear_all_breakpoints_requested = Signal() + sig_clear_breakpoint_requested = Signal(str, int) + sig_edit_goto_requested = Signal(str, int, str) + sig_set_or_edit_conditional_breakpoint_requested = Signal() def __init__(self, parent, data): super().__init__(parent) + + # Widgets self.model = BreakpointTableModel(self, data) - self.setModel(self.model) self.delegate = BreakpointDelegate(self) - self.setItemDelegate(self.delegate) - - self.setup_table() - def setup_table(self): - """Setup table""" - self.horizontalHeader().setStretchLastSection(True) + # Setup + self.setSortingEnabled(False) + self.setSelectionBehavior(self.SelectRows) + self.setSelectionMode(self.SingleSelection) + self.setModel(self.model) + self.setItemDelegate(self.delegate) self.adjust_columns() self.columnAt(0) - # Sorting columns - self.setSortingEnabled(False) - self.sortByColumn(COL_FILE, Qt.DescendingOrder) + self.horizontalHeader().setStretchLastSection(True) - def adjust_columns(self): - """Resize three first columns to contents""" - for col in range(COLUMN_COUNT-1): - self.resizeColumnToContents(col) + # --- SpyderWidgetMixin API + # ------------------------------------------------------------------------ + def setup(self, options={}): + clear_all_action = self.create_action( + BreakpointTableActions.ClearAllBreakpoints, + _("Clear breakpoints in all files"), + triggered=self.sig_clear_all_breakpoints_requested, + ) + clear_action = self.create_action( + BreakpointTableActions.ClearBreakpoint, + _("Clear selected breakpoint"), + triggered=self.clear_breakpoints, + ) + edit_action = self.create_action( + BreakpointTableActions.EditBreakpoint, + _("Edit selected breakpoint"), + triggered=self.edit_breakpoints, + ) + + self.popup_menu = self.create_menu(MainWidgetMenus.Context) + self.add_item_to_menu(clear_all_action, menu=self.popup_menu) + self.add_item_to_menu(clear_action, menu=self.popup_menu) + self.add_item_to_menu(edit_action, menu=self.popup_menu) + + # --- Qt overrides + # ------------------------------------------------------------------------ + def contextMenuEvent(self, event): + """ + Override Qt method. + """ + c_row = self.indexAt(event.pos()).row() + enabled = bool(self.model.breakpoints) and c_row is not None + clear_action = self.get_action( + BreakpointTableActions.ClearBreakpoint) + edit_action = self.get_action( + BreakpointTableActions.EditBreakpoint) + clear_action.setEnabled(enabled) + edit_action.setEnabled(enabled) + + self.popup_menu.popup(event.globalPos()) + event.accept() def mouseDoubleClickEvent(self, event): - """Reimplement Qt method""" + """ + Override Qt method. + """ index_clicked = self.indexAt(event.pos()) if self.model.breakpoints: c_row = index_clicked.row() filename = self.model.breakpoints[c_row][COL_FULL] line_number_str = self.model.breakpoints[c_row][COL_LINE] - self.edit_goto.emit(filename, int(line_number_str), '') + + self.sig_edit_goto_requested.emit( + filename, int(line_number_str), '') + if index_clicked.column() == COL_CONDITION: - self.set_or_edit_conditional_breakpoint.emit() + self.sig_set_or_edit_conditional_breakpoint_requested.emit() - def contextMenuEvent(self, event): - index_clicked = self.indexAt(event.pos()) - actions = [] - self.popup_menu = QMenu(self) - clear_all_breakpoints_action = create_action( - self, _("Clear breakpoints in all files"), - triggered=lambda: self.clear_all_breakpoints.emit()) - actions.append(clear_all_breakpoints_action) - if self.model.breakpoints: - c_row = index_clicked.row() + # --- API + # ------------------------------------------------------------------------ + def set_data(self, data): + """ + Set the model breakpoint data dictionary. + """ + self.model.set_data(data) + self.adjust_columns() + self.sortByColumn(COL_FILE, Qt.DescendingOrder) + + def adjust_columns(self): + """ + Resize three first columns to contents. + """ + for col in range(COLUMN_COUNT - 1): + self.resizeColumnToContents(col) + + def clear_breakpoints(self): + """ + Clear selected row breakpoint. + """ + rows = self.selectionModel().selectedRows() + if rows and self.model.breakpoints: + c_row = rows[0].row() filename = self.model.breakpoints[c_row][COL_FULL] lineno = int(self.model.breakpoints[c_row][COL_LINE]) - def clear_slot(filename=filename, lineno=lineno): - return self.clear_breakpoint.emit(filename, lineno) + self.sig_clear_breakpoint_requested.emit(filename, lineno) - def edit_slot(filename=filename, lineno=lineno): - self.edit_goto.emit(filename, lineno, '') - self.set_or_edit_conditional_breakpoint.emit() - - clear_breakpoint_action = create_action( - self, _("Clear this breakpoint"), - triggered=clear_slot) - actions.insert(0, clear_breakpoint_action) + def edit_breakpoints(self): + """ + Edit selected row breakpoint condition. + """ + rows = self.selectionModel().selectedRows() + if rows and self.model.breakpoints: + c_row = rows[0].row() + filename = self.model.breakpoints[c_row][COL_FULL] + lineno = int(self.model.breakpoints[c_row][COL_LINE]) - edit_breakpoint_action = create_action( - self, _("Edit this breakpoint"), - triggered=edit_slot) - actions.append(edit_breakpoint_action) - add_actions(self.popup_menu, actions) - self.popup_menu.popup(event.globalPos()) - event.accept() + self.sig_edit_goto_requested.emit(filename, lineno, '') + self.sig_set_or_edit_conditional_breakpoint_requested.emit() -class BreakpointWidget(QWidget): +class BreakpointWidget(PluginMainWidget): """ - Breakpoint widget + Breakpoints widget. """ - VERSION = '1.0.0' - clear_all_breakpoints = Signal() - set_or_edit_conditional_breakpoint = Signal() - clear_breakpoint = Signal(str, int) - edit_goto = Signal(str, int, str) - def __init__(self, parent, options_button=None): - super().__init__(parent) + DEFAULT_OPTIONS = {} - self.setWindowTitle("Breakpoints") - self.dictwidget = BreakpointTableView(self, - self._load_all_breakpoints()) - if options_button: - btn_layout = QHBoxLayout() - btn_layout.setAlignment(Qt.AlignLeft) - btn_layout.addStretch() - btn_layout.addWidget(options_button, Qt.AlignRight) - layout = create_plugin_layout(btn_layout, self.dictwidget) - else: - layout = QVBoxLayout() - layout.addWidget(self.dictwidget) + # Signals + sig_clear_all_breakpoints_requested = Signal() + sig_clear_breakpoint_requested = Signal(str, int) + sig_edit_goto_requested = Signal(str, int, str) + sig_set_or_edit_conditional_breakpoint_requested = Signal() + + def __init__(self, name=None, plugin=None, parent=None, + options=DEFAULT_OPTIONS): + super().__init__(name, plugin, parent, options) + + # Widgets + self.breakpoints_table = BreakpointTableView(self, {}) + + # Layout + layout = QVBoxLayout() + layout.addWidget(self.breakpoints_table) self.setLayout(layout) - self.dictwidget.clear_all_breakpoints.connect( - lambda: self.clear_all_breakpoints.emit()) - self.dictwidget.clear_breakpoint.connect( - lambda s1, lino: self.clear_breakpoint.emit(s1, lino)) - self.dictwidget.edit_goto.connect( - lambda s1, lino, s2: self.edit_goto.emit(s1, lino, s2)) - self.dictwidget.set_or_edit_conditional_breakpoint.connect( - lambda: self.set_or_edit_conditional_breakpoint.emit()) - - def _load_all_breakpoints(self): - bp_dict = CONF.get('run', 'breakpoints', {}) - for filename in list(bp_dict.keys()): - if not osp.isfile(filename): - bp_dict.pop(filename) - return bp_dict - - def get_data(self): - pass - - def set_data(self): - bp_dict = self._load_all_breakpoints() - self.dictwidget.model.set_data(bp_dict) - self.dictwidget.adjust_columns() - self.dictwidget.sortByColumn(COL_FILE, Qt.DescendingOrder) + + # Signals + bpt = self.breakpoints_table + bpt.sig_clear_all_breakpoints_requested.connect( + self.sig_clear_all_breakpoints_requested) + bpt.sig_clear_breakpoint_requested.connect( + self.sig_clear_breakpoint_requested) + bpt.sig_edit_goto_requested.connect( + self.sig_edit_goto_requested) + bpt.sig_set_or_edit_conditional_breakpoint_requested.connect( + self.sig_set_or_edit_conditional_breakpoint_requested) + + # --- PluginMainWidget API + # ------------------------------------------------------------------------ + def setup(self, options): + self.breakpoints_table.setup() + + clear_all_action = self.get_action( + BreakpointTableActions.ClearAllBreakpoints) + clear_action = self.get_action( + BreakpointTableActions.ClearBreakpoint) + edit_action = self.get_action( + BreakpointTableActions.EditBreakpoint) + + options_menu = self.get_menu(MainWidgetMenus.Options) + self.add_item_to_menu(clear_all_action, menu=options_menu) + self.add_item_to_menu(clear_action, menu=options_menu) + self.add_item_to_menu(edit_action, menu=options_menu) + + def update_actions(self): + rows = self.breakpoints_table.selectionModel().selectedRows() + c_row = rows[0] if rows else None + + enabled = (bool(self.breakpoints_table.model.breakpoints) + and c_row is not None) + clear_action = self.get_action( + BreakpointTableActions.ClearBreakpoint) + edit_action = self.get_action( + BreakpointTableActions.EditBreakpoint) + clear_action.setEnabled(enabled) + edit_action.setEnabled(enabled) + + def get_focus_widget(self): + return self.breakpoints_table + + def get_title(self): + return _('Breakpoints') + + # --- API + # ------------------------------------------------------------------------ + def set_data(self, data): + """ + Set the breakpoint table data dictionary. + """ + self.breakpoints_table.set_data(data) # ============================================================================= @@ -273,7 +385,7 @@ def test(): """Run breakpoint widget test""" from spyder.utils.qthelpers import qapplication app = qapplication() - widget = BreakpointWidget(None) + widget = BreakpointWidget() widget.show() sys.exit(app.exec_())