def test_start_server(mocker, dark, under_home):
    """Test that .start_server() starts a process with the correct script file,
    notebook dir, and dark argument; that it stores the server process in
    `.servers`; and that it calls ._check_server_running()."""
    serverManager = ServerManager(dark)
    mock_check = mocker.patch.object(serverManager, '_check_server_started')
    mock_QProcess = mocker.patch(
        'spyder_notebook.utils.servermanager.QProcess', spec=QProcess)
    mocker.patch('spyder_notebook.utils.servermanager.get_home_dir',
                 return_value='foo')
    if under_home:
        filename = osp.join('foo', 'bar', 'ham.ipynb')
        nbdir = osp.join('foo')
    else:
        filename = osp.join('notfoo', 'bar', 'ham.ipynb')
        nbdir = osp.join('notfoo', 'bar')

    serverManager.start_server(filename)

    mock_QProcess.return_value.start.assert_called_once()
    args = mock_QProcess.return_value.start.call_args[0]
    import spyder_notebook.server.main
    assert args[1][0] == spyder_notebook.server.main.__file__
    assert '--notebook-dir={}'.format(nbdir) in args[1]
    assert ('--dark' in args[1]) == dark
    assert len(serverManager.servers) == 1
    assert serverManager.servers[0].process == mock_QProcess.return_value
    assert serverManager.servers[0].notebook_dir == nbdir
    mock_check.assert_called_once()
def test_get_server_with_server(mocker, nbdir, state, result, start):
    """Test that .get_server() returns a suitable server if it is accepting
    requests, and that it start up a new server unless a suitable server exists
    that is either starting up or running. Here, a suitable server is a server
    which renders the given notebook."""
    serverManager = ServerManager()
    mock_start = mocker.patch.object(serverManager, 'start_server')
    filename = osp.abspath('foo/ham.ipynb')
    server_info = mocker.Mock(spec=dict)
    server = ServerProcess(mocker.Mock(spec=QProcess),
                           osp.abspath(nbdir),
                           state=state,
                           server_info=server_info)
    serverManager.servers.append(server)

    res = serverManager.get_server(filename)

    if result:
        assert res == server_info
    else:
        assert res is None
    if start:
        mock_start.assert_called_once_with(filename)
    else:
        mock_start.assert_not_called()
def test_handle_finished(mocker, qtbot):
    """Test that .handle_finished() changes the state."""
    server = ServerProcess(mocker.Mock(spec=QProcess), '')
    serverManager = ServerManager()

    serverManager.handle_finished(server, mocker.Mock(), mocker.Mock())

    assert server.state == ServerState.FINISHED
def test_handle_error(mocker, qtbot):
    """Test that .handle_error() changes the state and emits signal."""
    server = ServerProcess(mocker.Mock(spec=QProcess), '')
    serverManager = ServerManager()

    with qtbot.waitSignal(serverManager.sig_server_errored):
        serverManager.handle_error(server, mocker.Mock())

    assert server.state == ServerState.ERROR
def test_get_server_without_servers(mocker, start_arg):
    """Test that .get_server() calls .start_server() when there are no servers
    only if `start` is True."""
    serverManager = ServerManager()
    mock_start = mocker.patch.object(serverManager, 'start_server')
    filename = osp.abspath('ham.ipynb')

    res = serverManager.get_server(filename, start=start_arg)

    assert res is None
    if start_arg:
        mock_start.assert_called_once_with(filename)
    else:
        mock_start.assert_not_called()
def test_read_standard_output(mocker):
    """Test that .read_standard_output() stores the output."""
    before = 'before\n'
    output = 'Αθήνα\n'  # check that we can handle non-ascii
    mock_read = mocker.Mock(return_value=QByteArray(output.encode()))
    mock_process = mocker.Mock(spec=QProcess, readAllStandardOutput=mock_read)
    server = ServerProcess(mock_process, '', output=before)
    serverManager = ServerManager()
    serverManager.servers = [server]

    serverManager.read_server_output(server)

    mock_read.assert_called_once()
    assert server.output == before + output
def test_check_server_started_if_errored(mocker, qtbot):
    """Test that .check_server_started() does not do anything if server state
    is ERROR."""
    fake_open = mocker.patch('spyder_notebook.utils.servermanager.open')
    mock_QTimer = mocker.patch('spyder_notebook.utils.servermanager.QTimer',
                               spec=QTimer)
    mock_process = mocker.Mock(spec=QProcess, processId=lambda: 7)
    server_process = ServerProcess(
        mock_process, 'notebookdir', 'interpreter', state=ServerState.ERROR)
    serverManager = ServerManager()

    serverManager._check_server_started(server_process)

    fake_open.assert_not_called()
    mock_QTimer.assert_not_called()
    assert server_process.state == ServerState.ERROR
def test_check_server_started_if_started(mocker, qtbot):
    """Test that .check_server_started() emits sig_server_started if there
    is a json file with the correct name and completes the server info."""
    fake_open = mocker.patch('spyder_notebook.utils.servermanager.open',
                             mocker.mock_open(read_data='{"foo": 42}'))
    mocker.patch('spyder_notebook.utils.servermanager.jupyter_runtime_dir',
                 return_value='runtimedir')
    mock_process = mocker.Mock(spec=QProcess, processId=lambda: 7)
    server_process = ServerProcess(mock_process, 'notebookdir', 'interpreter')
    serverManager = ServerManager()

    with qtbot.waitSignal(serverManager.sig_server_started):
        serverManager._check_server_started(server_process)

    fake_open.assert_called_once_with(
        osp.join('runtimedir', 'nbserver-7.json'), encoding='utf-8')
    assert server_process.state == ServerState.RUNNING
    assert server_process.server_info == {'foo': 42}
def test_shutdown_all_servers(mocker):
    """Test that .shutdown_all_servers() does shutdown all running servers,
    but not servers in another state."""
    mock_shutdown = mocker.patch(
        'spyder_notebook.utils.servermanager.notebookapp.shutdown_server')
    server1 = ServerProcess(
        mocker.Mock(spec=QProcess), '', '', state=ServerState.RUNNING,
        server_info=mocker.Mock(dict))
    server2 = ServerProcess(
        mocker.Mock(spec=QProcess), '', '', state=ServerState.ERROR,
        server_info=mocker.Mock(dict))
    serverManager = ServerManager()
    serverManager.servers = [server1, server2]

    serverManager.shutdown_all_servers()

    assert mock_shutdown.called_once_with(server1.server_info)
    assert server1.state == ServerState.FINISHED
    assert server2.state == ServerState.ERROR
def test_check_server_started_if_timed_out(mocker, qtbot):
    """Test that .check_server_started() emits sig_server_timed_out if after
    an hour there is still no json file."""
    fake_open = mocker.patch('spyder_notebook.utils.servermanager.open',
                             side_effect=OSError)
    mocker.patch('spyder_notebook.utils.servermanager.jupyter_runtime_dir',
                 return_value='runtimedir')
    mock_process = mocker.Mock(spec=QProcess, processId=lambda: 7)
    one_hour_ago = datetime.datetime.now() - datetime.timedelta(hours=1)
    server_process = ServerProcess(
        mock_process, 'notebookdir', 'interpreter', starttime=one_hour_ago)
    serverManager = ServerManager()

    with qtbot.waitSignal(serverManager.sig_server_timed_out):
        serverManager._check_server_started(server_process)

    fake_open.assert_called_once_with(
        osp.join('runtimedir', 'nbserver-7.json'), encoding='utf-8')
    assert server_process.state == ServerState.TIMED_OUT
def test_check_server_started_if_not_started(mocker, qtbot):
    """Test that .check_server_started() repeats itself on a timer if there
    is no json file with the correct name."""
    fake_open = mocker.patch('spyder_notebook.utils.servermanager.open',
                             side_effect=OSError)
    mocker.patch('spyder_notebook.utils.servermanager.jupyter_runtime_dir',
                 return_value='runtimedir')
    mock_QTimer = mocker.patch('spyder_notebook.utils.servermanager.QTimer',
                               spec=QTimer)
    mock_process = mocker.Mock(spec=QProcess, processId=lambda: 7)
    server_process = ServerProcess(mock_process, 'notebookdir', 'interpreter')
    serverManager = ServerManager()

    serverManager._check_server_started(server_process)

    fake_open.assert_called_once_with(
        osp.join('runtimedir', 'nbserver-7.json'), encoding='utf-8')
    assert server_process.state == ServerState.STARTING
    mock_QTimer.singleShot.assert_called_once()
示例#12
0
    def __init__(self, parent, testing=False):
        """Constructor."""
        if testing:
            self.CONF_FILE = False

        SpyderPluginWidget.__init__(self, parent)
        self.testing = testing

        self.fileswitcher_dlg = None
        self.main = parent

        self.recent_notebooks = self.get_option('recent_notebooks', default=[])
        self.recent_notebook_menu = QMenu(_("Open recent"), self)

        layout = QVBoxLayout()

        new_notebook_btn = create_toolbutton(self,
                                             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'))

        self.menu_actions = self.get_plugin_actions()
        menu_btn.setMenu(self._options_menu)
        menu_btn.setPopupMode(menu_btn.InstantPopup)
        corner_widgets = {Qt.TopRightCorner: [new_notebook_btn, menu_btn]}

        dark_theme = is_dark_interface()
        self.server_manager = ServerManager(dark_theme)
        self.tabwidget = NotebookTabWidget(
            self, self.server_manager, menu=self._options_menu,
            actions=self.menu_actions, corner_widgets=corner_widgets,
            dark_theme=dark_theme)

        self.tabwidget.currentChanged.connect(self.refresh_plugin)

        layout.addWidget(self.tabwidget)
        self.setLayout(layout)
示例#13
0
    def __init__(self, options):
        super().__init__()

        if options.dark:
            self.setStyleSheet(qdarkstyle.load_stylesheet_from_environment())

        self.server_manager = ServerManager(options.dark)
        QApplication.instance().aboutToQuit.connect(
            self.server_manager.shutdown_all_servers)

        self.tabwidget = NotebookTabWidget(self,
                                           self.server_manager,
                                           dark_theme=options.dark)

        if options.notebook:
            self.tabwidget.open_notebook(options.notebook)
        else:
            self.tabwidget.maybe_create_welcome_client()

        self.setCentralWidget(self.tabwidget)
        self._setup_menu()
示例#14
0
class NotebookPlugin(SpyderPluginWidget):
    """IPython Notebook plugin."""

    CONF_SECTION = 'notebook'
    CONF_DEFAULTS = [(CONF_SECTION, {
        'recent_notebooks': [],    # Items in "Open recent" menu
        'opened_notebooks': []})]  # Notebooks to open at start
    focus_changed = Signal()

    def __init__(self, parent, testing=False):
        """Constructor."""
        if testing:
            self.CONF_FILE = False

        SpyderPluginWidget.__init__(self, parent)
        self.testing = testing

        self.fileswitcher_dlg = None
        self.main = parent

        self.recent_notebooks = self.get_option('recent_notebooks', default=[])
        self.recent_notebook_menu = QMenu(_("Open recent"), self)

        layout = QVBoxLayout()

        new_notebook_btn = create_toolbutton(self,
                                             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'))

        self.menu_actions = self.get_plugin_actions()
        menu_btn.setMenu(self._options_menu)
        menu_btn.setPopupMode(menu_btn.InstantPopup)
        corner_widgets = {Qt.TopRightCorner: [new_notebook_btn, menu_btn]}

        dark_theme = is_dark_interface()
        self.server_manager = ServerManager(dark_theme)
        self.tabwidget = NotebookTabWidget(
            self, self.server_manager, menu=self._options_menu,
            actions=self.menu_actions, corner_widgets=corner_widgets,
            dark_theme=dark_theme)

        self.tabwidget.currentChanged.connect(self.refresh_plugin)

        layout.addWidget(self.tabwidget)
        self.setLayout(layout)

    # ------ SpyderPluginMixin API --------------------------------------------
    def on_first_registration(self):
        """Action to be performed on first plugin registration."""
        self.main.tabify_plugins(self.main.editor, self)

    def update_font(self):
        """Update font from Preferences."""
        # For now we're passing. We need to create an nbextension for
        # this.
        pass

    # ------ SpyderPluginWidget API -------------------------------------------
    def get_plugin_title(self):
        """Return widget title."""
        title = _('Notebook')
        return title

    def get_plugin_icon(self):
        """Return widget icon."""
        return ima.icon('ipython_console')

    def get_focus_widget(self):
        """Return the widget to give focus to."""
        client = self.tabwidget.currentWidget()
        if client is not None:
            return client.notebookwidget

    def closing_plugin(self, cancelable=False):
        """
        Perform actions before parent main window is closed.

        This function closes all tabs. It stores the file names of all opened
        notebooks that are not temporary and all notebooks in the 'Open recent'
        menu in the config.
        """
        opened_notebooks = []
        for client_index in range(self.tabwidget.count()):
            client = self.tabwidget.widget(client_index)
            if (not self.tabwidget.is_welcome_client(client)
                    and not self.tabwidget.is_newly_created(client)):
                opened_notebooks.append(client.filename)
            client.close()

        self.set_option('recent_notebooks', self.recent_notebooks)
        self.set_option('opened_notebooks', opened_notebooks)
        self.server_manager.shutdown_all_servers()
        return True

    def refresh_plugin(self):
        """Refresh tabwidget."""
        nb = None
        if self.tabwidget.count():
            client = self.tabwidget.currentWidget()
            nb = client.notebookwidget
            nb.setFocus()
        else:
            nb = None
        self.update_notebook_actions()

    def get_plugin_actions(self):
        """Return a list of actions related to plugin."""
        create_nb_action = create_action(
            self, _("New notebook"), icon=ima.icon('filenew'),
            triggered=self.create_new_client)
        self.save_as_action = create_action(
            self, _("Save as..."), icon=ima.icon('filesaveas'),
            triggered=self.save_as)
        open_action = create_action(
            self, _("Open..."), icon=ima.icon('fileopen'),
            triggered=self.open_notebook)
        self.open_console_action = create_action(
            self, _("Open console"), icon=ima.icon('ipython_console'),
            triggered=self.open_console)
        self.server_info_action = create_action(
            self, _('Server info...'), icon=ima.icon('log'),
            triggered=self.view_servers)
        self.clear_recent_notebooks_action = create_action(
            self, _("Clear this list"), triggered=self.clear_recent_notebooks)

        # Plugin actions
        self.menu_actions = [
            create_nb_action, open_action, self.recent_notebook_menu,
            MENU_SEPARATOR, self.save_as_action, MENU_SEPARATOR,
            self.open_console_action, MENU_SEPARATOR,
            self.server_info_action]
        self.setup_menu_actions()

        return self.menu_actions

    def register_plugin(self):
        """Register plugin in Spyder's main window."""
        super().register_plugin()
        self.focus_changed.connect(self.main.plugin_focus_changed)
        self.ipyconsole = self.main.ipyconsole

        # Open initial tabs
        filenames = self.get_option('opened_notebooks')
        if filenames:
            self.open_notebook(filenames)
        else:
            self.tabwidget.maybe_create_welcome_client()
            self.create_new_client()
            self.tabwidget.setCurrentIndex(0)  # bring welcome tab to top

        # 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):
        """Check compatibility for PyQt and sWebEngine."""
        message = ''
        value = True
        if PYQT4 or PYSIDE:
            message = _("You are working with Qt4 and in order to use this "
                        "plugin you need to have Qt5.<br><br>"
                        "Please update your Qt and/or PyQt packages to "
                        "meet this requirement.")
            value = False
        return value, message

    # ------ Public API (for clients) -----------------------------------------
    def setup_menu_actions(self):
        """Setup and update the menu actions."""
        self.recent_notebook_menu.clear()
        self.recent_notebooks_actions = []
        if self.recent_notebooks:
            for notebook in self.recent_notebooks:
                name = notebook
                action = \
                    create_action(self,
                                  name,
                                  icon=ima.icon('filenew'),
                                  triggered=lambda v,
                                  path=notebook:
                                      self.create_new_client(filename=path))
                self.recent_notebooks_actions.append(action)
            self.recent_notebooks_actions += \
                [None, self.clear_recent_notebooks_action]
        else:
            self.recent_notebooks_actions = \
                [self.clear_recent_notebooks_action]
        add_actions(self.recent_notebook_menu, self.recent_notebooks_actions)
        self.update_notebook_actions()

    def update_notebook_actions(self):
        """Update actions of the recent notebooks menu."""
        if self.recent_notebooks:
            self.clear_recent_notebooks_action.setEnabled(True)
        else:
            self.clear_recent_notebooks_action.setEnabled(False)
        try:
            client = self.tabwidget.currentWidget()
        except AttributeError:  # tabwidget is not yet constructed
            client = None
        if client and not self.tabwidget.is_welcome_client(client):
            self.save_as_action.setEnabled(True)
            self.open_console_action.setEnabled(True)
        else:
            self.save_as_action.setEnabled(False)
            self.open_console_action.setEnabled(False)

    def add_to_recent(self, notebook):
        """
        Add an entry to recent notebooks.

        We only maintain the list of the 20 most recent notebooks.
        """
        if notebook not in self.recent_notebooks:
            self.recent_notebooks.insert(0, notebook)
            self.recent_notebooks = self.recent_notebooks[:20]

    def clear_recent_notebooks(self):
        """Clear the list of recent notebooks."""
        self.recent_notebooks = []
        self.setup_menu_actions()

    def create_new_client(self, filename=None):
        """Create a new notebook or load a pre-existing one."""
        # Save spyder_pythonpath before creating a client
        # because it's needed by our kernel spec.
        if not self.testing:
            self.set_option('main/spyder_pythonpath',
                            self.main.get_spyder_pythonpath())

        client = self.tabwidget.create_new_client(filename)
        if not self.tabwidget.is_newly_created(client):
            self.add_to_recent(filename)
            self.setup_menu_actions()

    def open_notebook(self, filenames=None):
        """Open a notebook from file."""
        # Save spyder_pythonpath before creating a client
        # because it's needed by our kernel spec.
        if not self.testing:
            self.set_option('main/spyder_pythonpath',
                            self.main.get_spyder_pythonpath())

        filenames = self.tabwidget.open_notebook(filenames)
        for filename in filenames:
            self.add_to_recent(filename)
        self.setup_menu_actions()

    def save_as(self):
        """Save current notebook to different file."""
        self.tabwidget.save_as()

    def open_console(self, client=None):
        """Open an IPython console for the given client or the current one."""
        if not client:
            client = self.tabwidget.currentWidget()
        if self.ipyconsole is not None:
            kernel_id = client.get_kernel_id()
            if not kernel_id:
                QMessageBox.critical(
                    self, _('Error opening console'),
                    _('There is no kernel associated to this notebook.'))
                return
            self.ipyconsole._create_client_for_kernel(kernel_id, None, None,
                                                      None)
            ipyclient = self.ipyconsole.get_current_client()
            ipyclient.allow_rename = False
            self.ipyconsole.rename_client_tab(ipyclient,
                                              client.get_short_name())

    def view_servers(self):
        """Display server info."""
        dialog = ServerInfoDialog(self.server_manager.servers, parent=self)
        dialog.show()

    # ------ Public API (for FileSwitcher) ------------------------------------
    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

        clients = [self.tabwidget.widget(i)
                   for i in range(self.tabwidget.count())]
        paths = [client.get_filename() for client in clients]
        is_unsaved = [False for client in 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, clients):
            title = osp.basename(path)
            description = osp.dirname(path)
            if len(path) > 75:
                description = short_path
            is_last_item = (client == 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.tabwidget.indexOf(client)
        self.tabwidget.setCurrentIndex(index)
        self.switch_to_plugin()
        self.switcher.hide()