From 47be76713c25cd1b6c842a959a36d32397a27d7d Mon Sep 17 00:00:00 2001 From: Cheryl Sabella Date: Fri, 29 Sep 2017 14:18:40 -0400 Subject: [PATCH 1/3] History plugin: Add an option to show/hide line numbers --- spyder/config/main.py | 1 + spyder/defaults/defaults-2.4.0.ini | 1 + spyder/defaults/defaults-3.0.0.ini | 1 + spyder/plugins/history.py | 36 +++- spyder/plugins/tests/test_history.py | 227 ++++++++++++++++++++++++ spyder/widgets/sourcecode/codeeditor.py | 6 +- 6 files changed, 265 insertions(+), 7 deletions(-) create mode 100644 spyder/plugins/tests/test_history.py diff --git a/spyder/config/main.py b/spyder/config/main.py index 3e41bb757a0..04943fe626e 100755 --- a/spyder/config/main.py +++ b/spyder/config/main.py @@ -231,6 +231,7 @@ 'max_entries': 100, 'wrap': True, 'go_to_eof': True, + 'line_numbers': False, }), ('help', { diff --git a/spyder/defaults/defaults-2.4.0.ini b/spyder/defaults/defaults-2.4.0.ini index e12ed44cd6f..13c185e87f9 100644 --- a/spyder/defaults/defaults-2.4.0.ini +++ b/spyder/defaults/defaults-2.4.0.ini @@ -215,6 +215,7 @@ font/italic = False wrap = True font/family = ['Monospace', 'DejaVu Sans Mono', 'Consolas', 'Monaco', 'Bitstream Vera Sans Mono', 'Andale Mono', 'Liberation Mono', 'Courier New', 'Courier', 'monospace', 'Fixed', 'Terminal'] shortcut = Ctrl+Shift+H +line_numbers = False [inspector] max_history_entries = 20 diff --git a/spyder/defaults/defaults-3.0.0.ini b/spyder/defaults/defaults-3.0.0.ini index 3e9c5970890..802de52d265 100644 --- a/spyder/defaults/defaults-3.0.0.ini +++ b/spyder/defaults/defaults-3.0.0.ini @@ -197,6 +197,7 @@ font/italic = False wrap = True font/family = ['Ubuntu Mono', 'Monospace', 'DejaVu Sans Mono', 'Consolas', 'Monaco', 'Bitstream Vera Sans Mono', 'Andale Mono', 'Liberation Mono', 'Courier New', 'Courier', 'monospace', 'Fixed', 'Terminal'] shortcut = Ctrl+Shift+H +line_numbers = False [inspector] max_history_entries = 20 diff --git a/spyder/plugins/history.py b/spyder/plugins/history.py index 6bca336b62f..fea42ee5260 100644 --- a/spyder/plugins/history.py +++ b/spyder/plugins/history.py @@ -43,6 +43,8 @@ def setup_page(self): sourcecode_group = QGroupBox(_("Source code")) wrap_mode_box = self.create_checkbox(_("Wrap lines"), 'wrap') + linenumbers_mode_box = self.create_checkbox(_("Show line numbers"), + 'line_numbers') go_to_eof_box = self.create_checkbox( _("Scroll automatically to last entry"), 'go_to_eof') @@ -52,6 +54,7 @@ def setup_page(self): sourcecode_layout = QVBoxLayout() sourcecode_layout.addWidget(wrap_mode_box) + sourcecode_layout.addWidget(linenumbers_mode_box) sourcecode_layout.addWidget(go_to_eof_box) sourcecode_group.setLayout(sourcecode_layout) @@ -70,16 +73,18 @@ class HistoryLog(SpyderPluginWidget): CONFIGWIDGET_CLASS = HistoryConfigPage focus_changed = Signal() - def __init__(self, parent): + def __init__(self, parent, testing=False): SpyderPluginWidget.__init__(self, parent) self.tabwidget = None self.menu_actions = None self.dockviewer = None self.wrap_action = None + self.linenumbers_action = None self.editors = [] self.filenames = [] + self.testing = testing # Initialize plugin self.initialize_plugin() @@ -111,7 +116,8 @@ def __init__(self, parent): # Find/replace widget self.find_widget = FindReplace(self) self.find_widget.hide() - self.register_widget_shortcuts(self.find_widget) + if not self.testing: + self.register_widget_shortcuts(self.find_widget) layout.addWidget(self.find_widget) @@ -147,14 +153,18 @@ def refresh_plugin(self): def get_plugin_actions(self): """Return a list of actions related to plugin""" - history_action = create_action(self, _("History..."), + self.history_action = create_action(self, _("History..."), None, ima.icon('history'), _("Set history maximum entries"), triggered=self.change_history_depth) self.wrap_action = create_action(self, _("Wrap lines"), toggled=self.toggle_wrap_mode) self.wrap_action.setChecked( self.get_option('wrap') ) - self.menu_actions = [history_action, self.wrap_action] + self.linenumbers_action = create_action( + self, _("Show line numbers"), toggled=self.toggle_line_numbers) + self.linenumbers_action.setChecked(self.get_option('line_numbers')) + self.menu_actions = [self.history_action, self.wrap_action, + self.linenumbers_action] return self.menu_actions def on_first_registration(self): @@ -184,6 +194,8 @@ def apply_plugin_settings(self, options): wrap_n = 'wrap' wrap_o = self.get_option(wrap_n) self.wrap_action.setChecked(wrap_o) + linenb_n = 'line_numbers' + linenb_o = self.get_option(linenb_n) for editor in self.editors: if font_n in options: scs = color_scheme_o if color_scheme_n in options else None @@ -192,7 +204,9 @@ def apply_plugin_settings(self, options): editor.set_color_scheme(color_scheme_o) if wrap_n in options: editor.toggle_wrap_mode(wrap_o) - + if linenb_n in options: + editor.toggle_line_numbers(linenumbers=linenb_o, markers=False) + #------ Private API -------------------------------------------------------- def move_tab(self, index_from, index_to): """ @@ -218,7 +232,8 @@ def add_history(self, filename): language = 'py' else: language = 'bat' - editor.setup_editor(linenumbers=False, language=language, + editor.setup_editor(linenumbers=self.get_option('line_numbers'), + language=language, scrollflagarea=False) editor.focus_changed.connect(lambda: self.focus_changed.emit()) editor.setReadOnly(True) @@ -270,3 +285,12 @@ def toggle_wrap_mode(self, checked): for editor in self.editors: editor.toggle_wrap_mode(checked) self.set_option('wrap', checked) + + @Slot(bool) + def toggle_line_numbers(self, checked): + """Toggle line numbers.""" + if self.tabwidget is None: + return + for editor in self.editors: + editor.toggle_line_numbers(linenumbers=checked, markers=False) + self.set_option('line_numbers', checked) diff --git a/spyder/plugins/tests/test_history.py b/spyder/plugins/tests/test_history.py new file mode 100644 index 00000000000..bb156813866 --- /dev/null +++ b/spyder/plugins/tests/test_history.py @@ -0,0 +1,227 @@ +# -*- coding: utf-8 -*- +# +# Copyright © Spyder Project Contributors +# Licensed under the terms of the MIT License +# + +import pytest + +from qtpy.QtGui import QTextOption + +from spyder.plugins import history + + +#============================================================================== +# Constants +#============================================================================== + + +#============================================================================== +# Utillity Functions +#============================================================================== +options = {'wrap': False, + 'line_numbers': False, + 'go_to_eof': True, + 'max_entries': 100} + + +def get_option(self, option): + global options + return options[option] + + +def set_option(self, option, value): + global options + options[option] = value + + +#============================================================================== +# Qt Test Fixtures +#============================================================================== +@pytest.fixture +def historylog(qtbot): + historylog = history.HistoryLog(None, testing=True) + qtbot.addWidget(historylog) + historylog.show() + yield historylog + historylog.closing_plugin() + historylog.close() + + +@pytest.fixture +def historylog_with_tab(historylog, mocker, monkeypatch): + hl = historylog + # Mock read so file doesn't have to exist. + mocker.patch.object(history.encoding, 'read') + history.encoding.read.return_value = ('', '') + + # Monkeypatch current options. + monkeypatch.setattr(history.HistoryLog, 'get_option', get_option) + monkeypatch.setattr(history.HistoryLog, 'set_option', set_option) + + # Create tab for page. + hl.set_option('wrap', False) + hl.set_option('line_numbers', False) + hl.set_option('max_entries', 100) + hl.set_option('go_to_eof', True) + hl.add_history('test_history.py') + return hl + + +#============================================================================== +# Tests +#============================================================================== + +def test_init(historylog): + hl = historylog + assert hl.editors == [] + assert hl.filenames == [] + assert hl.plugin_actions == hl.menu_actions + assert hl.tabwidget.menu.actions() == hl.menu_actions + assert hl.tabwidget.cornerWidget().menu().actions() == hl.menu_actions + + +def test_add_history(historylog, mocker, monkeypatch): + hl = historylog + hle = hl.editors + + # Mock read so file doesn't have to exist. + mocker.patch.object(history.encoding, 'read') + + # Monkeypatch current options. + monkeypatch.setattr(history.HistoryLog, 'get_option', get_option) + monkeypatch.setattr(history.HistoryLog, 'set_option', set_option) + # No editors yet. + assert len(hl.editors) == 0 + + # Add one file. + tab1 = 'test_history.py' + text1 = 'a = 5\nb= 10\na + b\n' + hl.set_option('line_numbers', False) + hl.set_option('wrap', False) + history.encoding.read.return_value = (text1, '') + hl.add_history(tab1) + assert len(hle) == 1 + assert hl.filenames == [tab1] + assert hl.tabwidget.currentIndex() == 0 + assert not hle[0].linenumberarea.isVisible() + assert hle[0].wordWrapMode() == QTextOption.NoWrap + assert hl.tabwidget.tabText(0) == tab1 + assert hl.tabwidget.tabToolTip(0) == tab1 + + hl.set_option('line_numbers', True) + hl.set_option('wrap', True) + # Try to add same file -- does not process filename again, so + # linenumbers and wrap doesn't change. + hl.add_history(tab1) + assert hl.tabwidget.currentIndex() == 0 + assert not hl.editors[0].linenumberarea.isVisible() + + # Add another file. + tab2 = 'history2.js' + text2 = 'random text\nspam line\n\n\n\n' + history.encoding.read.return_value = (text2, '') + hl.add_history(tab2) + assert len(hle) == 2 + assert hl.filenames == [tab1, tab2] + assert hl.tabwidget.currentIndex() == 1 + assert hle[1].linenumberarea.isVisible() + assert hle[1].wordWrapMode() == QTextOption.WrapAtWordBoundaryOrAnywhere + assert hl.tabwidget.tabText(1) == tab2 + assert hl.tabwidget.tabToolTip(1) == tab2 + + assert hl.filenames == [tab1, tab2] + + assert hle[0].supported_language + assert hle[0].is_python() + assert hle[0].isReadOnly() + assert not hle[0].isVisible() + assert hle[0].toPlainText() == text1 + + assert not hle[1].supported_language + assert not hle[1].is_python() + assert hle[1].isReadOnly() + assert hle[1].isVisible() + assert hle[1].toPlainText() == text2 + + +def test_append_to_history(historylog_with_tab, mocker): + hl = historylog_with_tab + + hl.set_option('go_to_eof', True) + hl.editors[0].set_cursor_position('sof') + hl.append_to_history('test_history.py', 'import re\n') + assert hl.editors[0].toPlainText() == 'import re\n' + assert hl.tabwidget.currentIndex() == 0 + assert hl.editors[0].is_cursor_at_end() + assert not hl.editors[0].linenumberarea.isVisible() + + hl.set_option('go_to_eof', False) + hl.editors[0].set_cursor_position('sof') + hl.append_to_history('test_history.py', 'a = r"[a-z]"\n') + assert hl.editors[0].toPlainText() == 'import re\na = r"[a-z]"\n' + assert not hl.editors[0].is_cursor_at_end() + + +def test_change_history_depth(historylog_with_tab, mocker): + hl = historylog_with_tab + action = hl.history_action + # Mock dialog. + mocker.patch.object(history.QInputDialog, 'getInt') + + # Starts with default. + assert hl.get_option('max_entries') == 100 + + # Invalid data. + history.QInputDialog.getInt.return_value = (10, False) + action.trigger() + assert hl.get_option('max_entries') == 100 # No change. + + # Valid data. + history.QInputDialog.getInt.return_value = (475, True) + action.trigger() + assert hl.get_option('max_entries') == 475 + + +def test_toggle_wrap_mode(historylog_with_tab): + hl = historylog_with_tab + action = hl.wrap_action + action.setChecked(False) + + # Starts with wrap mode off. + assert hl.editors[0].wordWrapMode() == QTextOption.NoWrap + assert not hl.get_option('wrap') + + # Toggles wrap mode on. + action.setChecked(True) + assert hl.editors[0].wordWrapMode() == QTextOption.WrapAtWordBoundaryOrAnywhere + assert hl.get_option('wrap') + + # Toggles wrap mode off. + action.setChecked(False) + assert hl.editors[0].wordWrapMode() == QTextOption.NoWrap + assert not hl.get_option('wrap') + + +def test_toggle_line_numbers(historylog_with_tab): + hl = historylog_with_tab + action = hl.linenumbers_action + action.setChecked(False) + + # Starts without line numbers. + assert not hl.editors[0].linenumberarea.isVisible() + assert not hl.get_option('line_numbers') + + # Toggles line numbers on. + action.setChecked(True) + assert hl.editors[0].linenumberarea.isVisible() + assert hl.get_option('line_numbers') + + # Toggles line numbers off. + action.setChecked(False) + assert not hl.editors[0].linenumberarea.isVisible() + assert not hl.get_option('line_numbers') + + +if __name__ == "__main__": + pytest.main() diff --git a/spyder/widgets/sourcecode/codeeditor.py b/spyder/widgets/sourcecode/codeeditor.py index 5d6b2b011d6..91631c6d256 100644 --- a/spyder/widgets/sourcecode/codeeditor.py +++ b/spyder/widgets/sourcecode/codeeditor.py @@ -613,6 +613,10 @@ def toggle_wrap_mode(self, enable): """Enable/disable wrap mode""" self.set_wrap_mode('word' if enable else None) + def toggle_line_numbers(self, linenumbers=True, markers=False): + """Enable/disable line numbers.""" + self.linenumberarea.setup_margins(linenumbers, markers) + @property def panels(self): """ @@ -671,7 +675,7 @@ def setup_editor(self, linenumbers=True, language=None, markers=False, # Line number area if cloned_from: self.setFont(font) # this is required for line numbers area - self.linenumberarea.setup_margins(linenumbers, markers) + self.toggle_line_numbers(linenumbers, markers) # Lexer self.set_language(language, filename) From 9d329b8d815b31e4aa61853e6ff8219f5c268801 Mon Sep 17 00:00:00 2001 From: Cheryl Sabella Date: Fri, 29 Sep 2017 14:24:18 -0400 Subject: [PATCH 2/3] Remove changes to defaults file --- spyder/defaults/defaults-2.4.0.ini | 1 - spyder/defaults/defaults-3.0.0.ini | 1 - 2 files changed, 2 deletions(-) diff --git a/spyder/defaults/defaults-2.4.0.ini b/spyder/defaults/defaults-2.4.0.ini index 13c185e87f9..e12ed44cd6f 100644 --- a/spyder/defaults/defaults-2.4.0.ini +++ b/spyder/defaults/defaults-2.4.0.ini @@ -215,7 +215,6 @@ font/italic = False wrap = True font/family = ['Monospace', 'DejaVu Sans Mono', 'Consolas', 'Monaco', 'Bitstream Vera Sans Mono', 'Andale Mono', 'Liberation Mono', 'Courier New', 'Courier', 'monospace', 'Fixed', 'Terminal'] shortcut = Ctrl+Shift+H -line_numbers = False [inspector] max_history_entries = 20 diff --git a/spyder/defaults/defaults-3.0.0.ini b/spyder/defaults/defaults-3.0.0.ini index 802de52d265..3e9c5970890 100644 --- a/spyder/defaults/defaults-3.0.0.ini +++ b/spyder/defaults/defaults-3.0.0.ini @@ -197,7 +197,6 @@ font/italic = False wrap = True font/family = ['Ubuntu Mono', 'Monospace', 'DejaVu Sans Mono', 'Consolas', 'Monaco', 'Bitstream Vera Sans Mono', 'Andale Mono', 'Liberation Mono', 'Courier New', 'Courier', 'monospace', 'Fixed', 'Terminal'] shortcut = Ctrl+Shift+H -line_numbers = False [inspector] max_history_entries = 20 From d37322a6a3752fefd699ea128f06f4396c05885c Mon Sep 17 00:00:00 2001 From: Cheryl Sabella Date: Wed, 4 Oct 2017 16:26:39 -0400 Subject: [PATCH 3/3] Changes per requests. --- spyder/plugins/history.py | 6 +-- spyder/plugins/tests/test_history.py | 59 +++++++++++++++++++++++----- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/spyder/plugins/history.py b/spyder/plugins/history.py index fea42ee5260..fa1c45cc14a 100644 --- a/spyder/plugins/history.py +++ b/spyder/plugins/history.py @@ -73,7 +73,7 @@ class HistoryLog(SpyderPluginWidget): CONFIGWIDGET_CLASS = HistoryConfigPage focus_changed = Signal() - def __init__(self, parent, testing=False): + def __init__(self, parent): SpyderPluginWidget.__init__(self, parent) self.tabwidget = None @@ -84,7 +84,6 @@ def __init__(self, parent, testing=False): self.editors = [] self.filenames = [] - self.testing = testing # Initialize plugin self.initialize_plugin() @@ -116,8 +115,7 @@ def __init__(self, parent, testing=False): # Find/replace widget self.find_widget = FindReplace(self) self.find_widget.hide() - if not self.testing: - self.register_widget_shortcuts(self.find_widget) + self.register_widget_shortcuts(self.find_widget) layout.addWidget(self.find_widget) diff --git a/spyder/plugins/tests/test_history.py b/spyder/plugins/tests/test_history.py index bb156813866..f5d6590fa8a 100644 --- a/spyder/plugins/tests/test_history.py +++ b/spyder/plugins/tests/test_history.py @@ -10,12 +10,6 @@ from spyder.plugins import history - -#============================================================================== -# Constants -#============================================================================== - - #============================================================================== # Utillity Functions #============================================================================== @@ -34,13 +28,16 @@ def set_option(self, option, value): global options options[option] = value - #============================================================================== # Qt Test Fixtures #============================================================================== @pytest.fixture -def historylog(qtbot): - historylog = history.HistoryLog(None, testing=True) +def historylog(qtbot, monkeypatch): + """Return a fixture for base history log, which is a plugin widget.""" + monkeypatch.setattr(history.HistoryLog, + 'register_widget_shortcuts', + lambda *args: None) + historylog = history.HistoryLog(None) qtbot.addWidget(historylog) historylog.show() yield historylog @@ -50,6 +47,13 @@ def historylog(qtbot): @pytest.fixture def historylog_with_tab(historylog, mocker, monkeypatch): + """Return a fixture for a history log with one tab. + + The base history log is a plugin widget. Within the plugin widget, + the method add_history creates a tab containing a code editor + for each history file. This fixture creates a history log with + one tab containing no text. + """ hl = historylog # Mock read so file doesn't have to exist. mocker.patch.object(history.encoding, 'read') @@ -67,12 +71,16 @@ def historylog_with_tab(historylog, mocker, monkeypatch): hl.add_history('test_history.py') return hl - #============================================================================== # Tests #============================================================================== def test_init(historylog): + """Test HistoryLog.__init__. + + Test that the initialization created the expected instance variables + and widgets for a new HistoryLog instance. + """ hl = historylog assert hl.editors == [] assert hl.filenames == [] @@ -82,6 +90,11 @@ def test_init(historylog): def test_add_history(historylog, mocker, monkeypatch): + """Test the add_history method. + + Test adding a history file to the history log widget and the + code editor properties that are enabled/disabled. + """ hl = historylog hle = hl.editors @@ -101,6 +114,7 @@ def test_add_history(historylog, mocker, monkeypatch): hl.set_option('wrap', False) history.encoding.read.return_value = (text1, '') hl.add_history(tab1) + # Check tab and editor were created correctly. assert len(hle) == 1 assert hl.filenames == [tab1] assert hl.tabwidget.currentIndex() == 0 @@ -122,6 +136,7 @@ def test_add_history(historylog, mocker, monkeypatch): text2 = 'random text\nspam line\n\n\n\n' history.encoding.read.return_value = (text2, '') hl.add_history(tab2) + # Check second tab and editor were created correctly. assert len(hle) == 2 assert hl.filenames == [tab1, tab2] assert hl.tabwidget.currentIndex() == 1 @@ -132,6 +147,7 @@ def test_add_history(historylog, mocker, monkeypatch): assert hl.filenames == [tab1, tab2] + # Check differences between tabs based on setup. assert hle[0].supported_language assert hle[0].is_python() assert hle[0].isReadOnly() @@ -146,24 +162,39 @@ def test_add_history(historylog, mocker, monkeypatch): def test_append_to_history(historylog_with_tab, mocker): + """Test the append_to_history method. + + Test adding text to a history file. Also test the go_to_eof config + option for positioning the cursor. + """ hl = historylog_with_tab + # Toggle to move to the end of the file after appending. hl.set_option('go_to_eof', True) + # Force cursor to the beginning of the file. hl.editors[0].set_cursor_position('sof') hl.append_to_history('test_history.py', 'import re\n') assert hl.editors[0].toPlainText() == 'import re\n' assert hl.tabwidget.currentIndex() == 0 + # Cursor moved to end. assert hl.editors[0].is_cursor_at_end() assert not hl.editors[0].linenumberarea.isVisible() + # Toggle to not move cursor after appending. hl.set_option('go_to_eof', False) + # Force cursor to the beginning of the file. hl.editors[0].set_cursor_position('sof') hl.append_to_history('test_history.py', 'a = r"[a-z]"\n') assert hl.editors[0].toPlainText() == 'import re\na = r"[a-z]"\n' + # Cursor not at end. assert not hl.editors[0].is_cursor_at_end() def test_change_history_depth(historylog_with_tab, mocker): + """Test the change_history_depth method. + + Modify the 'Maximum history entries' values to test the config action. + """ hl = historylog_with_tab action = hl.history_action # Mock dialog. @@ -184,6 +215,10 @@ def test_change_history_depth(historylog_with_tab, mocker): def test_toggle_wrap_mode(historylog_with_tab): + """Test the toggle_wrap_mode method. + + Toggle the 'Wrap lines' config action. + """ hl = historylog_with_tab action = hl.wrap_action action.setChecked(False) @@ -204,6 +239,10 @@ def test_toggle_wrap_mode(historylog_with_tab): def test_toggle_line_numbers(historylog_with_tab): + """Test toggle_line_numbers method. + + Toggle the 'Show line numbers' config action. + """ hl = historylog_with_tab action = hl.linenumbers_action action.setChecked(False)