From 6ac8c5629963d119fc8f9b776c6e79c191ee7c74 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Tue, 24 Sep 2019 14:49:43 +0100 Subject: [PATCH 1/6] Spyder 4 compatibility fixes This also removes all workarounds to keep compatibility with Spyder 3, so the minimum version required is now v4. As an (unwanted) side-effect, this commit removes the possibility to pick notebooks using the file switcher. The API for this has changed and a future commit will have to re-introduce the switcher. --- setup.py | 2 +- spyder_notebook/notebookplugin.py | 53 +++++++++++-------------------- spyder_notebook/widgets/client.py | 17 +++------- 3 files changed, 24 insertions(+), 48 deletions(-) diff --git a/setup.py b/setup.py index 0acaf975..038c8a54 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ def get_version(module='spyder_notebook'): return version -REQUIREMENTS = ['spyder>=3.2.0', 'notebook>=4.3', +REQUIREMENTS = ['spyder>=4', 'notebook>=4.3', 'qtpy', 'requests', 'psutil', 'nbformat'] setup( diff --git a/spyder_notebook/notebookplugin.py b/spyder_notebook/notebookplugin.py index ec2ab32a..9e625c4b 100644 --- a/spyder_notebook/notebookplugin.py +++ b/spyder_notebook/notebookplugin.py @@ -15,35 +15,20 @@ from qtpy import PYQT4, PYSIDE from qtpy.compat import getsavefilename, getopenfilenames from qtpy.QtCore import Qt, QEventLoop, QTimer, Signal -from qtpy.QtGui import QIcon from qtpy.QtWidgets import QApplication, QMessageBox, QVBoxLayout, QMenu # Third-party imports import nbformat # Spyder imports +from spyder.api.plugins import SpyderPluginWidget from spyder.config.base import _ -from spyder.config.main import CONF from spyder.utils import icon_manager as ima +from spyder.utils.programs import get_temp_dir from spyder.utils.qthelpers import (create_action, create_toolbutton, add_actions, MENU_SEPARATOR) from spyder.widgets.tabs import Tabs -try: - # Spyder >= 3.3.2 - from spyder.utils.programs import get_temp_dir -except ImportError: - # Spyder < 3.3.2 - from spyder.utils.programs import TEMPDIR - - def get_temp_dir(): - return TEMPDIR -try: - # Spyder 4 - from spyder.api.plugins import SpyderPluginWidget -except ImportError: - # Spyder 3 - from spyder.plugins import SpyderPluginWidget # Local imports from .utils.nbopen import nbopen, NBServerError @@ -65,6 +50,9 @@ class NotebookPlugin(SpyderPluginWidget): def __init__(self, parent, testing=False): """Constructor.""" + if testing: + self.CONF_FILE = False + SpyderPluginWidget.__init__(self, parent) self.testing = testing @@ -80,21 +68,17 @@ def __init__(self, parent, testing=False): self.recent_notebook_menu = QMenu(_("Open recent"), self) self.options_menu = QMenu(self) - # Initialize plugin - self.initialize_plugin() - layout = QVBoxLayout() new_notebook_btn = create_toolbutton(self, - icon=ima.icon('project_expanded'), - tip=_('Open a new notebook'), - triggered=self.create_new_client) + icon=ima.icon('options_more'), + tip=_('Open a new notebook'), + triggered=self.create_new_client) menu_btn = create_toolbutton(self, icon=ima.icon('tooloptions'), - tip=_('Options')) + tip=_('Options')) menu_btn.setMenu(self.options_menu) menu_btn.setPopupMode(menu_btn.InstantPopup) - add_actions(self.options_menu, self.menu_actions) corner_widgets = {Qt.TopRightCorner: [new_notebook_btn, menu_btn]} self.tabwidget = Tabs(self, menu=self.options_menu, actions=self.menu_actions, @@ -192,13 +176,14 @@ def get_plugin_actions(self): def register_plugin(self): """Register plugin in Spyder's main window.""" + super(NotebookPlugin, self).register_plugin() self.focus_changed.connect(self.main.plugin_focus_changed) - self.main.add_dockwidget(self) self.ipyconsole = self.main.ipyconsole self.create_new_client(give_focus=False) - icon_path = os.path.join(PACKAGE_PATH, 'images', 'icon.svg') - self.main.add_to_fileswitcher(self, self.tabwidget, self.clients, - QIcon(icon_path)) + # TODO Convert to new Switcher + # icon_path = os.path.join(PACKAGE_PATH, 'images', 'icon.svg') + # self.main.add_to_fileswitcher(self, self.tabwidget, self.clients, + # QIcon(icon_path)) self.recent_notebook_menu.aboutToShow.connect(self.setup_menu_actions) def check_compatibility(self): @@ -321,8 +306,8 @@ def create_new_client(self, filename=None, give_focus=True): # Save spyder_pythonpath before creating a client # because it's needed by our kernel spec. if not self.testing: - CONF.set('main', 'spyder_pythonpath', - self.main.get_spyder_pythonpath()) + self.set_option('main/spyder_pythonpath', + self.main.get_spyder_pythonpath()) # Open the notebook with nbopen and get the url we need to render try: @@ -461,11 +446,9 @@ def add_tab(self, widget): index = self.tabwidget.addTab(widget, widget.get_short_name()) self.tabwidget.setCurrentIndex(index) self.tabwidget.setTabToolTip(index, widget.get_filename()) - if self.dockwidget and not self.ismaximized: - self.dockwidget.setVisible(True) - self.dockwidget.raise_() + if self.dockwidget: + self.switch_to_plugin() self.activateWindow() - widget.notebookwidget.setFocus() def move_tab(self, index_from, index_to): """Move tab.""" diff --git a/spyder_notebook/widgets/client.py b/spyder_notebook/widgets/client.py index 708cc246..1801ab97 100644 --- a/spyder_notebook/widgets/client.py +++ b/spyder_notebook/widgets/client.py @@ -45,18 +45,11 @@ except NameError: FileNotFoundError = IOError # Python 2 -try: - # Spyder 4 - PLUGINS_PATH = get_module_source_path('spyder', 'plugins') - CSS_PATH = osp.join(PLUGINS_PATH, 'help', 'utils', 'static', 'css') - TEMPLATES_PATH = osp.join( - PLUGINS_PATH, 'ipythonconsole', 'assets', 'templates') - open(osp.join(TEMPLATES_PATH, 'blank.html')) -except FileNotFoundError: - # Spyder 3 - UTILS_PATH = get_module_source_path('spyder', 'utils') - CSS_PATH = osp.join(UTILS_PATH, 'help', 'static', 'css') - TEMPLATES_PATH = osp.join(UTILS_PATH, 'ipython', 'templates') +PLUGINS_PATH = get_module_source_path('spyder', 'plugins') +CSS_PATH = osp.join(PLUGINS_PATH, 'help', 'utils', 'static', 'css') +TEMPLATES_PATH = osp.join( + PLUGINS_PATH, 'ipythonconsole', 'assets', 'templates') +open(osp.join(TEMPLATES_PATH, 'blank.html')) BLANK = open(osp.join(TEMPLATES_PATH, 'blank.html')).read() LOADING = open(osp.join(TEMPLATES_PATH, 'loading.html')).read() From 08fef516b5bbeafa77851745119b34874af027e7 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Wed, 25 Sep 2019 10:42:27 +0100 Subject: [PATCH 2/6] Remove .show() from test This causes a segfault when running the test locally and does not seem necessary for testing. --- spyder_notebook/tests/test_plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/spyder_notebook/tests/test_plugin.py b/spyder_notebook/tests/test_plugin.py index b1d37c62..ad72447e 100644 --- a/spyder_notebook/tests/test_plugin.py +++ b/spyder_notebook/tests/test_plugin.py @@ -107,7 +107,6 @@ def notebook(qtbot): notebook_plugin = NotebookPlugin(None, testing=True) qtbot.addWidget(notebook_plugin) notebook_plugin.create_new_client() - notebook_plugin.show() return notebook_plugin From bba332320725aa0c25164d180adc795958e7486d Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Wed, 25 Sep 2019 11:46:45 +0100 Subject: [PATCH 3/6] Update CI scripts and unify with other Spyder plugin scripts --- .circleci/config.yml | 31 +++++++--------- .circleci/install.sh | 43 ---------------------- .circleci/run-tests.sh | 12 ------ .travis.yml | 30 +++++++++++++++ continuous_integration/circle/coverage.sh | 7 ++++ continuous_integration/circle/install.sh | 29 +++++++++++++++ continuous_integration/circle/run_tests.sh | 7 ++++ requirements/conda.txt | 2 + requirements/tests.txt | 6 +++ 9 files changed, 95 insertions(+), 72 deletions(-) delete mode 100755 .circleci/install.sh delete mode 100755 .circleci/run-tests.sh create mode 100644 .travis.yml create mode 100755 continuous_integration/circle/coverage.sh create mode 100755 continuous_integration/circle/install.sh create mode 100755 continuous_integration/circle/run_tests.sh create mode 100644 requirements/conda.txt create mode 100644 requirements/tests.txt diff --git a/.circleci/config.yml b/.circleci/config.yml index 8739d67a..308e86f3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,40 +2,37 @@ version: 2 main: &main machine: true - environment: - # Used by qthelpers to close widgets after a defined time - - TEST_CI: "True" - - TEST_CI_APP: "True" steps: - checkout - run: command: docker pull dorowu/ubuntu-desktop-lxde-vnc:trusty - run: - name: Install required pachages - command: ./.circleci/install.sh + name: Install system packages + command: | + sudo apt-get -qq update + sudo apt-get install -q libegl1-mesa + - run: + command: bash ./continuous_integration/circle/install.sh - run: - name: Run tests - command: ./.circleci/run-tests.sh + command: bash ./continuous_integration/circle/run_tests.sh + - run: + command: bash ./continuous_integration/circle/coverage.sh jobs: python2.7: <<: *main environment: - - PYTHON_VERSION: 2.7 - SPYDER_BRANCH: 3.x + - PYTHON_VERSION: "2.7" python3.6: <<: *main environment: - - PYTHON_VERSION: 3.6 - SPYDER_BRANCH: 3.x + - PYTHON_VERSION: "3.6" - python3.7_spyder4: + python3.7: <<: *main environment: - - PYTHON_VERSION: 3.7 - SPYDER_BRANCH: master - + - PYTHON_VERSION: "3.7" workflows: version: 2 @@ -43,4 +40,4 @@ workflows: jobs: - python2.7 - python3.6 - - python3.7_spyder4 + - python3.7 diff --git a/.circleci/install.sh b/.circleci/install.sh deleted file mode 100755 index 8b613f84..00000000 --- a/.circleci/install.sh +++ /dev/null @@ -1,43 +0,0 @@ -#! /bin/bash -ex -# -e means: Exit immediately if a command exits with a non-zero status -# -x means: Print commands and their arguments as they are executed. - -# Install some required system packages -sudo apt-get update -sudo apt-get install libegl1-mesa - -# Install Miniconda -wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh -bash miniconda.sh -b -p $HOME/miniconda -source $HOME/miniconda/etc/profile.d/conda.sh - -# Make new conda environment with required Python version -conda create -y -n test python=$PYTHON_VERSION -conda activate test - -# Install Spyder's dependencies -conda install -y --only-deps spyder - -# Download Spyder's source (3.x branch) from github -mkdir spyder-source -pushd spyder-source -wget -q https://github.com/spyder-ide/spyder/archive/$SPYDER_BRANCH.zip -unzip -q $SPYDER_BRANCH.zip -popd - -# Install Spyder from source -pushd spyder-source/spyder-$SPYDER_BRANCH -python setup.py install -popd - -# Install spyder-notebook dependency -# Specify Qt 5 to prevent Qt 4 being installed with Python 2 -conda install -y notebook qt=5 - -# Install testing dependencies -conda install -y pytest pytest-cov pytest-mock flaky -pip install coveralls pytest-qt - -# List packages for debugging -echo '********** output of conda list **********' -conda list diff --git a/.circleci/run-tests.sh b/.circleci/run-tests.sh deleted file mode 100755 index afea7d87..00000000 --- a/.circleci/run-tests.sh +++ /dev/null @@ -1,12 +0,0 @@ -#! /bin/bash -ex -# -e means: Exit immediately if a command exits with a non-zero status -# -x means: Print commands and their arguments as they are executed. - -source $HOME/miniconda/etc/profile.d/conda.sh -conda activate test - -# Run tests; flag -x means stop on first test failure -pytest -x -vv spyder_notebook --cov=spyder_notebook - -# Generate coverage report -COVERALLS_REPO_TOKEN=Kr503QwklmJYKXYRXLywrtw8zbX7K8SKx coveralls diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..c24074e1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,30 @@ +# https://travis-ci.org/spyder-ide/spyder-notebook + +language: generic +dist: xenial +services: + - xvfb + +matrix: + fast_finish: true + include: + - env: PYTHON_VERSION=2.7 USE_CONDA=yes + - env: PYTHON_VERSION=3.6 USE_CONDA=yes + - env: PYTHON_VERSION=3.7 USE_CONDA=yes + +before_install: + # Avoid annoying focus problems when running tests + # See discussion in e.g. https://github.com/spyder-ide/spyder/pull/6132 + - sudo apt-get -qq update + - sudo apt-get install -y matchbox-window-manager xterm libxkbcommon-x11-0 + - matchbox-window-manager& + - sleep 5 + +install: + - ./continuous_integration/circle/install.sh + +script: + - ./continuous_integration/circle/run_tests.sh + +after_success: + - ./continuous_integration/circle/coverage.sh diff --git a/continuous_integration/circle/coverage.sh b/continuous_integration/circle/coverage.sh new file mode 100755 index 00000000..d6cb2fdd --- /dev/null +++ b/continuous_integration/circle/coverage.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +export COVERALLS_REPO_TOKEN=Kr503QwklmJYKXYRXLywrtw8zbX7K8SKx +source $HOME/miniconda/etc/profile.d/conda.sh +conda activate test + +coveralls diff --git a/continuous_integration/circle/install.sh b/continuous_integration/circle/install.sh new file mode 100755 index 00000000..cb762801 --- /dev/null +++ b/continuous_integration/circle/install.sh @@ -0,0 +1,29 @@ +#!/bin/bash -ex + +# -- Install Miniconda +MINICONDA=Miniconda3-latest-Linux-x86_64.sh +wget https://repo.continuum.io/miniconda/$MINICONDA -O miniconda.sh +bash miniconda.sh -b -p $HOME/miniconda +source $HOME/miniconda/etc/profile.d/conda.sh + + +# -- Make new conda environment with required Python version +conda create -y -n test python=$PYTHON_VERSION +conda activate test + + +# -- Install dependencies + +# Avoid problems with invalid SSL certificates +if [ "$PYTHON_VERSION" = "2.7" ]; then + conda install -q -y python=2.7.16=h9bab390_0 +fi + +# Install nomkl to avoid installing Intel MKL libraries +conda install -q -y nomkl + +# Install main dependencies +conda install -q -y -c spyder-ide --file requirements/conda.txt + +# Install test ones +conda install -q -y -c spyder-ide --file requirements/tests.txt diff --git a/continuous_integration/circle/run_tests.sh b/continuous_integration/circle/run_tests.sh new file mode 100755 index 00000000..147631ac --- /dev/null +++ b/continuous_integration/circle/run_tests.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +source $HOME/miniconda/etc/profile.d/conda.sh +conda activate test + +# Run tests +pytest -x -vv --cov=spyder_notebook spyder_notebook diff --git a/requirements/conda.txt b/requirements/conda.txt new file mode 100644 index 00000000..829406be --- /dev/null +++ b/requirements/conda.txt @@ -0,0 +1,2 @@ +notebook +spyder >=4.0.0b5 diff --git a/requirements/tests.txt b/requirements/tests.txt new file mode 100644 index 00000000..0d311a07 --- /dev/null +++ b/requirements/tests.txt @@ -0,0 +1,6 @@ +coveralls +flaky +mock +pytest +pytest-cov +pytest-qt From 255aec986256b6b9ebdb57848015d52b32116c29 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Wed, 25 Sep 2019 11:58:24 +0100 Subject: [PATCH 4/6] Code style: change indent --- spyder_notebook/notebookplugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spyder_notebook/notebookplugin.py b/spyder_notebook/notebookplugin.py index 9e625c4b..ea5eec3e 100644 --- a/spyder_notebook/notebookplugin.py +++ b/spyder_notebook/notebookplugin.py @@ -71,11 +71,11 @@ def __init__(self, parent, testing=False): layout = QVBoxLayout() new_notebook_btn = create_toolbutton(self, - icon=ima.icon('options_more'), - tip=_('Open a new notebook'), - triggered=self.create_new_client) + icon=ima.icon('options_more'), + tip=_('Open a new notebook'), + triggered=self.create_new_client) menu_btn = create_toolbutton(self, icon=ima.icon('tooloptions'), - tip=_('Options')) + tip=_('Options')) menu_btn.setMenu(self.options_menu) menu_btn.setPopupMode(menu_btn.InstantPopup) From 70454cc01eefa226c5132e3a7ded02e2ba142898 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Wed, 25 Sep 2019 12:00:21 +0100 Subject: [PATCH 5/6] Add pytest-mock as testing requirement --- requirements/tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/tests.txt b/requirements/tests.txt index 0d311a07..9b2ad4c5 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -1,6 +1,6 @@ coveralls flaky -mock pytest pytest-cov +pytest-mock pytest-qt From ce39ab1a8d0b2fda9469a074082366dd4710029f Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Thu, 26 Sep 2019 15:27:00 +0100 Subject: [PATCH 6/6] Interface to new Switcher introduced in Spyder 4 List all opened notebooks in the switcher, and switch to them if selected by the user. --- spyder_notebook/notebookplugin.py | 63 +++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/spyder_notebook/notebookplugin.py b/spyder_notebook/notebookplugin.py index ea5eec3e..2ec3445e 100644 --- a/spyder_notebook/notebookplugin.py +++ b/spyder_notebook/notebookplugin.py @@ -15,6 +15,7 @@ from qtpy import PYQT4, PYSIDE from qtpy.compat import getsavefilename, getopenfilenames from qtpy.QtCore import Qt, QEventLoop, QTimer, Signal +from qtpy.QtGui import QIcon from qtpy.QtWidgets import QApplication, QMessageBox, QVBoxLayout, QMenu # Third-party imports @@ -27,6 +28,7 @@ from spyder.utils.programs import get_temp_dir from spyder.utils.qthelpers import (create_action, create_toolbutton, add_actions, MENU_SEPARATOR) +from spyder.utils.switcher import shorten_paths from spyder.widgets.tabs import Tabs @@ -180,10 +182,13 @@ def register_plugin(self): self.focus_changed.connect(self.main.plugin_focus_changed) self.ipyconsole = self.main.ipyconsole self.create_new_client(give_focus=False) - # TODO Convert to new Switcher - # icon_path = os.path.join(PACKAGE_PATH, 'images', 'icon.svg') - # self.main.add_to_fileswitcher(self, self.tabwidget, self.clients, - # QIcon(icon_path)) + + # Connect to switcher + self.switcher = self.main.switcher + self.switcher.sig_mode_selected.connect(self.handle_switcher_modes) + self.switcher.sig_item_selected.connect( + self.handle_switcher_selection) + self.recent_notebook_menu.aboutToShow.connect(self.setup_menu_actions) def check_compatibility(self): @@ -456,11 +461,45 @@ def move_tab(self, index_from, index_to): self.clients.insert(index_to, client) # ------ Public API (for FileSwitcher) ------------------------------------ - def set_stack_index(self, index, instance): - """Set the index of the current notebook.""" - if instance == self: - self.tabwidget.setCurrentIndex(index) - - def get_current_tab_manager(self): - """Get the widget with the TabWidget attribute.""" - return self + def handle_switcher_modes(self, mode): + """ + Populate switcher with opened notebooks. + + List the file names of the opened notebooks with their directories in + the switcher. Only handle file mode, where `mode` is empty string. + """ + if mode != '': + return + + paths = [client.get_filename() for client in self.clients] + is_unsaved = [False for client in self.clients] + short_paths = shorten_paths(paths, is_unsaved) + icon = QIcon(os.path.join(PACKAGE_PATH, 'images', 'icon.svg')) + section = self.get_plugin_title() + + for path, short_path, client in zip(paths, short_paths, self.clients): + title = osp.basename(path) + description = osp.dirname(path) + if len(path) > 75: + description = short_path + is_last_item = (client == self.clients[-1]) + self.switcher.add_item( + title=title, description=description, icon=icon, + section=section, data=client, last_item=is_last_item) + + def handle_switcher_selection(self, item, mode, search_text): + """ + Handle user selecting item in switcher. + + If the selected item is not in the section of the switcher that + corresponds to this plugin, then ignore it. Otherwise, switch to + selected item in notebook plugin and hide the switcher. + """ + if item.get_section() != self.get_plugin_title(): + return + + client = item.get_data() + index = self.clients.index(client) + self.tabwidget.setCurrentIndex(index) + self.switch_to_plugin() + self.switcher.hide()