diff --git a/.github/workflows/test-linux.yml b/.github/workflows/test-linux.yml
index 067c2b5e260..6a39bcd2ad8 100644
--- a/.github/workflows/test-linux.yml
+++ b/.github/workflows/test-linux.yml
@@ -16,10 +16,10 @@ on:
- '**.sh'
pull_request:
- branches:
- - master
- - 5.*
- - 4.x
+ # branches:
+ # - master
+ # - 5.*
+ # - 4.x
paths:
- '.github/scripts/*.sh'
- '.github/workflows/*.yml'
diff --git a/.github/workflows/test-mac.yml b/.github/workflows/test-mac.yml
index 37c2dbcde4f..95a74ab2c35 100644
--- a/.github/workflows/test-mac.yml
+++ b/.github/workflows/test-mac.yml
@@ -16,10 +16,10 @@ on:
- '**.sh'
pull_request:
- branches:
- - master
- - 5.*
- - 4.x
+ # branches:
+ # - master
+ # - 5.*
+ # - 4.x
paths:
- '.github/scripts/*.sh'
- '.github/workflows/*.yml'
diff --git a/.github/workflows/test-win.yml b/.github/workflows/test-win.yml
index c5f39301066..70c2f8bc776 100644
--- a/.github/workflows/test-win.yml
+++ b/.github/workflows/test-win.yml
@@ -16,10 +16,10 @@ on:
- '**.sh'
pull_request:
- branches:
- - master
- - 5.*
- - 4.x
+ # branches:
+ # - master
+ # - 5.*
+ # - 4.x
paths:
- '.github/scripts/*.sh'
- '.github/workflows/*.yml'
diff --git a/spyder/api/plugin_registration/_confpage.py b/spyder/api/plugin_registration/_confpage.py
new file mode 100644
index 00000000000..9da92ecf3e5
--- /dev/null
+++ b/spyder/api/plugin_registration/_confpage.py
@@ -0,0 +1,117 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""Plugin registry configuration page."""
+
+# Third party imports
+from qtpy.QtWidgets import (QGroupBox, QVBoxLayout, QCheckBox,
+ QGridLayout, QLabel)
+
+# Local imports
+from spyder.api.plugins import SpyderPlugin
+from spyder.api.preferences import PluginConfigPage
+from spyder.config.base import _
+from spyder.config.manager import CONF
+
+
+class PluginsConfigPage(PluginConfigPage):
+ def setup_page(self):
+ newcb = self.create_checkbox
+ self.plugins_checkboxes = {}
+
+ header_label = QLabel(
+ _("Here you can turn on/off any internal or external Spyder plugin "
+ "to disable functionality that is not desired or to have a lighter "
+ "experience. Unchecked plugins in this page will be unloaded "
+ "immediately and will not be loaded the next time Spyder starts."))
+ header_label.setWordWrap(True)
+
+ # ------------------ Internal plugin status group ---------------------
+ internal_layout = QGridLayout()
+ self.internal_plugins_group = QGroupBox(_("Internal plugins"))
+
+ i = 0
+ for plugin_name in self.plugin.all_internal_plugins:
+ (conf_section_name,
+ PluginClass) = self.plugin.all_internal_plugins[plugin_name]
+
+ if not getattr(PluginClass, 'CAN_BE_DISABLED', True):
+ # Do not list core plugins that can not be disabled
+ continue
+
+ plugin_loc_name = None
+ if hasattr(PluginClass, 'get_name'):
+ plugin_loc_name = PluginClass.get_name()
+ elif hasattr(PluginClass, 'get_plugin_title'):
+ plugin_loc_name = PluginClass.get_plugin_title()
+
+ plugin_state = CONF.get(conf_section_name, 'enable', True)
+ cb = newcb(plugin_loc_name, 'enable', default=True,
+ section=conf_section_name, restart=True)
+ internal_layout.addWidget(cb, i // 2, i % 2)
+ self.plugins_checkboxes[plugin_name] = (cb, plugin_state)
+ i += 1
+
+ self.internal_plugins_group.setLayout(internal_layout)
+
+ # ------------------ External plugin status group ---------------------
+ external_layout = QGridLayout()
+ self.external_plugins_group = QGroupBox(_("External plugins"))
+
+ i = 0
+ for i, plugin_name in enumerate(self.plugin.all_external_plugins):
+ (conf_section_name,
+ PluginClass) = self.plugin.all_external_plugins[plugin_name]
+
+ plugin_loc_name = None
+ if hasattr(PluginClass, 'get_name'):
+ plugin_loc_name = PluginClass.get_name()
+ elif hasattr(PluginClass, 'get_plugin_title'):
+ plugin_loc_name = PluginClass.get_plugin_title()
+
+ cb = newcb(plugin_loc_name, 'enable', default=True,
+ section=conf_section_name, restart=True)
+ external_layout.addWidget(cb, i // 2, i % 2)
+ self.plugins_checkboxes[plugin_name] = cb
+ i += 1
+
+ self.external_plugins_group.setLayout(external_layout)
+
+ layout = QVBoxLayout()
+ layout.addWidget(header_label)
+ layout.addWidget(self.internal_plugins_group)
+ if self.plugin.all_external_plugins:
+ layout.addWidget(self.external_plugins_group)
+ layout.addStretch(1)
+ self.setLayout(layout)
+
+ def apply_settings(self):
+ for plugin_name in self.plugins_checkboxes:
+ cb, previous_state = self.plugins_checkboxes[plugin_name]
+ if cb.isChecked() and not previous_state:
+ self.plugin.set_plugin_enabled(plugin_name)
+ PluginClass = None
+ external = False
+ if plugin_name in self.plugin.all_internal_plugins:
+ (__,
+ PluginClass) = self.plugin.all_internal_plugins[plugin_name]
+ elif plugin_name in self.plugin.all_external_plugins:
+ (__,
+ PluginClass) = self.plugin.all_external_plugins[plugin_name]
+ external = True
+
+ # TODO: Once we can test that all plugins can be restarted
+ # without problems during runtime, we can enable the
+ # autorestart feature provided by the plugin registry:
+ # self.plugin.register_plugin(self.main, PluginClass,
+ # external=external)
+ elif not cb.isChecked() and previous_state:
+ # TODO: Once we can test that all plugins can be restarted
+ # without problems during runtime, we can enable the
+ # autorestart feature provided by the plugin registry:
+ # self.plugin.delete_plugin(plugin_name)
+ pass
+ return set({})
diff --git a/spyder/api/plugin_registration/registry.py b/spyder/api/plugin_registration/registry.py
index 0f7102c5426..b8a1437f17c 100644
--- a/spyder/api/plugin_registration/registry.py
+++ b/spyder/api/plugin_registration/registry.py
@@ -8,17 +8,23 @@
# Standard library imports
import logging
-from typing import Dict, List, Union, Type, Any, Set, Optional
+from typing import Dict, List, Union, Type, Any, Set, Optional, Tuple
# Third-party library imports
from qtpy.QtCore import QObject, Signal
# Local imports
+from spyder import dependencies
+from spyder.config.base import _, running_under_pytest
from spyder.config.manager import CONF
+from spyder.api.config.mixins import SpyderConfigurationAccessor
+from spyder.api.plugin_registration._confpage import PluginsConfigPage
+from spyder.api.plugins.enum import Plugins
from spyder.api.exceptions import SpyderAPIError
from spyder.api.plugins import (
Plugins, SpyderPluginV2, SpyderDockablePlugin, SpyderPluginWidget,
SpyderPlugin)
+from spyder.utils.icon_manager import ima
# TODO: Remove SpyderPlugin and SpyderPluginWidget once the migration
@@ -34,7 +40,23 @@
logger = logging.getLogger(__name__)
-class SpyderPluginRegistry(QObject):
+class PreferencesAdapter(SpyderConfigurationAccessor):
+ # Fake class constants used to register the configuration page
+ CONF_WIDGET_CLASS = PluginsConfigPage
+ NAME = 'plugin_registry'
+ CONF_VERSION = None
+ ADDITIONAL_CONF_OPTIONS = None
+ ADDITIONAL_CONF_TABS = None
+ CONF_SECTION = ""
+
+ def apply_plugin_settings(self, _unused):
+ pass
+
+ def apply_conf(self, _unused):
+ pass
+
+
+class SpyderPluginRegistry(QObject, PreferencesAdapter):
"""
Global plugin registry.
@@ -66,6 +88,11 @@ class SpyderPluginRegistry(QObject):
def __init__(self):
super().__init__()
+ PreferencesAdapter.__init__(self)
+
+ # Reference to the main window
+ self.main = None
+
# Dictionary that maps a plugin name to a list of the plugin names
# that depend on it.
self.plugin_dependents = {} # type: Dict[str, Dict[str, List[str]]]
@@ -92,6 +119,12 @@ def __init__(self):
# Set that stores the names of the external plugins
self.external_plugins = set({}) # type: set[str]
+ # Dictionary that contains all the internal plugins (enabled or not)
+ self.all_internal_plugins = {} # type: Dict[str, Tuple[str, Type[SpyderPluginClass]]]
+
+ # Dictionary that contains all the external plugins (enabled or not)
+ self.all_external_plugins = {} # type: Dict[str, Tuple[str, Type[SpyderPluginClass]]]
+
# ------------------------- PRIVATE API -----------------------------------
def _update_dependents(self, plugin: str, dependent_plugin: str, key: str):
"""Add `dependent_plugin` to the list of dependents of `plugin`."""
@@ -170,6 +203,15 @@ def _instantiate_spyder5_plugin(
else:
self.internal_plugins |= {plugin_name}
+ if external:
+ # These attributes come from spyder.app.find_plugins
+ module = PluginClass._spyder_module_name
+ package_name = PluginClass._spyder_package_name
+ version = PluginClass._spyder_version
+ description = plugin_instance.get_description()
+ dependencies.add(module, package_name, description,
+ version, None, kind=dependencies.PLUGIN)
+
return plugin_instance
def _instantiate_spyder4_plugin(
@@ -344,6 +386,10 @@ def notify_plugin_availability(self, plugin_name: str,
plugin_instance = self.plugin_registry[plugin]
plugin_instance._on_plugin_available(plugin_name)
+ if plugin_name == Plugins.Preferences and not running_under_pytest():
+ plugin_instance = self.plugin_registry[plugin_name]
+ plugin_instance.register_plugin_preferences(self)
+
def delete_plugin(self, plugin_name: str) -> bool:
"""
Remove and delete a plugin from the registry by its name.
@@ -418,7 +464,8 @@ def delete_plugin(self, plugin_name: str) -> bool:
self.plugin_registry.pop(plugin_name)
return True
- def delete_all_plugins(self, excluding: Optional[Set[str]] = None) -> bool:
+ def delete_all_plugins(self, excluding: Optional[Set[str]] = None,
+ close_immediately: bool = False) -> bool:
"""
Remove all plugins from the registry.
@@ -430,6 +477,8 @@ def delete_all_plugins(self, excluding: Optional[Set[str]] = None) -> bool:
----------
excluding: Optional[Set[str]]
A set that lists plugins (by name) that will not be deleted.
+ close_immediately: bool
+ If true, then the `can_close` status will be ignored.
Returns
-------
@@ -445,7 +494,7 @@ def delete_all_plugins(self, excluding: Optional[Set[str]] = None) -> bool:
plugin_instance = self.plugin_registry[plugin_name]
if isinstance(plugin_instance, SpyderPlugin):
can_close &= self.delete_plugin(plugin_name)
- if not can_close:
+ if not can_close and not close_immediately:
break
if not can_close:
@@ -457,10 +506,10 @@ def delete_all_plugins(self, excluding: Optional[Set[str]] = None) -> bool:
plugin_instance = self.plugin_registry[plugin_name]
if isinstance(plugin_instance, SpyderPluginV2):
can_close &= self.delete_plugin(plugin_name)
- if not can_close:
+ if not can_close and not close_immediately:
break
- if not can_close:
+ if not can_close and not close_immediately:
return False
# Delete Spyder 4 internal plugins
@@ -469,7 +518,7 @@ def delete_all_plugins(self, excluding: Optional[Set[str]] = None) -> bool:
plugin_instance = self.plugin_registry[plugin_name]
if isinstance(plugin_instance, SpyderPlugin):
can_close &= self.delete_plugin(plugin_name)
- if not can_close:
+ if not can_close and not close_immediately:
break
if not can_close:
@@ -480,7 +529,7 @@ def delete_all_plugins(self, excluding: Optional[Set[str]] = None) -> bool:
plugin_instance = self.plugin_registry[plugin_name]
if isinstance(plugin_instance, SpyderPluginV2):
can_close &= self.delete_plugin(plugin_name)
- if not can_close:
+ if not can_close and not close_immediately:
break
return can_close
@@ -589,6 +638,23 @@ def reset(self):
# Omit failures if there are no slots connected
pass
+ def set_all_internal_plugins(
+ self, all_plugins: Dict[str, Type[SpyderPluginClass]]):
+ self.all_internal_plugins = all_plugins
+
+ def set_all_external_plugins(
+ self, all_plugins: Dict[str, Type[SpyderPluginClass]]):
+ self.all_external_plugins = all_plugins
+
+ def set_main(self, main):
+ self.main = main
+
+ def get_icon(self):
+ return ima.icon('plugins')
+
+ def get_name(self):
+ return _('Plugins')
+
def __contains__(self, plugin_name: str) -> bool:
"""
Determine if a plugin name is contained in the registry.
diff --git a/spyder/api/plugins/new_api.py b/spyder/api/plugins/new_api.py
index 564e59f9274..8ae380d6382 100644
--- a/spyder/api/plugins/new_api.py
+++ b/spyder/api/plugins/new_api.py
@@ -603,7 +603,7 @@ def initialize(self):
Notes
-----
This method should be called to initialize the plugin, but it should
- not be overriden, since it internally calls `on_initialize` and emits
+ not be overridden, since it internally calls `on_initialize` and emits
the `sig_plugin_ready` signal.
"""
self.on_initialize()
@@ -650,7 +650,8 @@ def get_font(cls, rich_text=False):
# --- API: Mandatory methods to define -----------------------------------
# ------------------------------------------------------------------------
- def get_name(self):
+ @staticmethod
+ def get_name():
"""
Return the plugin localized name.
diff --git a/spyder/api/preferences.py b/spyder/api/preferences.py
index 85a04361b04..70602e81d4b 100644
--- a/spyder/api/preferences.py
+++ b/spyder/api/preferences.py
@@ -82,9 +82,13 @@ class PluginConfigPage(SpyderConfigPage):
def __init__(self, plugin, parent):
self.plugin = plugin
- self.CONF_SECTION = plugin.CONF_SECTION
self.main = parent.main
- self.get_font = plugin.get_font
+
+ if hasattr(plugin, 'CONF_SECTION'):
+ self.CONF_SECTION = plugin.CONF_SECTION
+
+ if hasattr(plugin, 'get_font'):
+ self.get_font = plugin.get_font
if not self.APPLY_CONF_PAGE_SETTINGS:
self._patch_apply_settings(plugin)
@@ -124,6 +128,12 @@ def aggregate_sections_partials(self, opts):
"""Aggregate options by sections in order to notify observers."""
to_update = {}
for opt in opts:
+ if isinstance(opt, tuple):
+ # This is necessary to filter tuple options that do not
+ # belong to a section.
+ if len(opt) == 2 and opt[0] is None:
+ opt = opt[1]
+
section = self.CONF_SECTION
if opt in self.cross_section_options:
section = self.cross_section_options[opt]
diff --git a/spyder/api/widgets/menus.py b/spyder/api/widgets/menus.py
index a83b736614b..11590318e0c 100644
--- a/spyder/api/widgets/menus.py
+++ b/spyder/api/widgets/menus.py
@@ -168,6 +168,20 @@ def add_action(self: T,
self._dirty = True
self._actions_map[item_id] = action
+ def remove_action(self, item_id: str):
+ if item_id in self._actions_map:
+ action = self._actions_map.pop(item_id)
+ position = None
+
+ for i, (_, act) in enumerate(self._actions):
+ if act == action:
+ position = i
+ break
+
+ if position is not None:
+ self._actions.pop(position)
+ self._dirty = True
+
def get_title(self):
"""
Return the title for menu.
diff --git a/spyder/api/widgets/toolbars.py b/spyder/api/widgets/toolbars.py
index 8a87cf15727..3c9912bb4c3 100644
--- a/spyder/api/widgets/toolbars.py
+++ b/spyder/api/widgets/toolbars.py
@@ -210,6 +210,18 @@ def add_item(self, action_or_widget: ToolbarItem,
self.add_item(item, section=section, before=before,
before_section=before_section)
+ def remove_item(self, item_id: str):
+ """Remove action or widget from toolbar by id."""
+ item = self._item_map.pop(item_id)
+ for section in list(self._section_items.keys()):
+ section_items = self._section_items[section]
+ if item in section_items:
+ section_items.remove(item)
+ if len(section_items) == 0:
+ self._section_items.pop(section)
+ self.clear()
+ self._render()
+
def _render(self):
"""
Create the toolbar taking into account sections and locations.
diff --git a/spyder/app/mainwindow.py b/spyder/app/mainwindow.py
index ca738e1cb82..ef1c26c3105 100644
--- a/spyder/app/mainwindow.py
+++ b/spyder/app/mainwindow.py
@@ -765,6 +765,8 @@ def setup(self):
lambda plugin_name, omit_conf: self.register_plugin(
plugin_name, omit_conf=omit_conf))
+ PLUGIN_REGISTRY.set_main(self)
+
# TODO: Remove circular dependency between help and ipython console
# and remove this import. Help plugin should take care of it
from spyder.plugins.help.utils.sphinxify import CSS_PATH, DARK_CSS_PATH
@@ -851,12 +853,20 @@ def setup(self):
# Determine 'enable' config for the plugins that have it
enabled_plugins = {}
+ registry_internal_plugins = {}
+ registry_external_plugins = {}
for plugin in all_plugins.values():
plugin_name = plugin.NAME
plugin_main_attribute_name = (
self._INTERNAL_PLUGINS_MAPPING[plugin_name]
if plugin_name in self._INTERNAL_PLUGINS_MAPPING
else plugin_name)
+ if plugin_name in internal_plugins:
+ registry_internal_plugins[plugin_name] = (
+ plugin_main_attribute_name, plugin)
+ else:
+ registry_external_plugins[plugin_name] = (
+ plugin_main_attribute_name, plugin)
try:
if CONF.get(plugin_main_attribute_name, "enable"):
enabled_plugins[plugin_name] = plugin
@@ -865,6 +875,9 @@ def setup(self):
enabled_plugins[plugin_name] = plugin
PLUGIN_REGISTRY.set_plugin_enabled(plugin_name)
+ PLUGIN_REGISTRY.set_all_internal_plugins(registry_internal_plugins)
+ PLUGIN_REGISTRY.set_all_external_plugins(registry_external_plugins)
+
# Instantiate internal Spyder 5 plugins
for plugin_name in internal_plugins:
if plugin_name in enabled_plugins:
@@ -896,15 +909,6 @@ def setup(self):
try:
plugin_instance = PLUGIN_REGISTRY.register_plugin(
self, PluginClass, external=True)
-
- # These attributes come from spyder.app.find_plugins to
- # add plugins to the dependencies dialog
- module = PluginClass._spyder_module_name
- package_name = PluginClass._spyder_package_name
- version = PluginClass._spyder_version
- description = plugin_instance.get_description()
- dependencies.add(module, package_name, description,
- version, None, kind=dependencies.PLUGIN)
except Exception as error:
print("%s: %s" % (PluginClass, str(error)), file=STDERR)
traceback.print_exc(file=STDERR)
@@ -1197,7 +1201,7 @@ def post_visible_setup(self):
# Show Help and Consoles by default
plugins_to_show = [self.ipyconsole]
- if self.help is not None:
+ if hasattr(self, 'help'):
plugins_to_show.append(self.help)
for plugin in plugins_to_show:
if plugin.dockwidget.isVisible():
@@ -1461,10 +1465,9 @@ def moveEvent(self, event):
if hasattr(self, 'layouts'):
if not self.isMaximized() and not self.layouts.get_fullscreen_flag():
self.window_position = self.pos()
- QMainWindow.moveEvent(self, event)
-
- # To be used by the tour to be able to move
- self.sig_moved.emit(event)
+ QMainWindow.moveEvent(self, event)
+ # To be used by the tour to be able to move
+ self.sig_moved.emit(event)
def hideEvent(self, event):
"""Reimplement Qt method"""
@@ -1495,7 +1498,7 @@ def change_last_focused_widget(self, old, now):
self.previous_focused_widget = old
- def closing(self, cancelable=False):
+ def closing(self, cancelable=False, close_immediately=False):
"""Exit tasks"""
if self.already_closed or self.is_starting_up:
return True
@@ -1511,9 +1514,9 @@ def closing(self, cancelable=False):
self.open_files_server.close()
can_close = PLUGIN_REGISTRY.delete_all_plugins(
- excluding={Plugins.Layout})
+ excluding={Plugins.Layout}, close_immediately=close_immediately)
- if not can_close:
+ if not can_close and not close_immediately:
return False
# Save window settings *after* closing all plugin windows, in order
@@ -1814,6 +1817,25 @@ def start_open_files_server(self):
self.sig_open_external_file.emit(fname)
req.sendall(b' ')
+ # ---- Quit and restart, and reset spyder defaults
+ @Slot()
+ def reset_spyder(self):
+ """
+ Quit and reset Spyder and then Restart application.
+ """
+ answer = QMessageBox.warning(self, _("Warning"),
+ _("Spyder will restart and reset to default settings:
"
+ "Do you want to continue?"),
+ QMessageBox.Yes | QMessageBox.No)
+ if answer == QMessageBox.Yes:
+ self.restart(reset=True)
+
+ @Slot()
+ def restart(self, reset=False, close_immediately=False):
+ """Wrapper to handle plugins request to restart Spyder."""
+ self.application.restart(
+ reset=reset, close_immediately=close_immediately)
+
# ---- Global Switcher
def open_switcher(self, symbol=False):
"""Open switcher dialog box."""
diff --git a/spyder/config/manager.py b/spyder/config/manager.py
index 9eedb99141b..124398994e5 100644
--- a/spyder/config/manager.py
+++ b/spyder/config/manager.py
@@ -614,6 +614,9 @@ def config_shortcut(self, action, context, name, parent):
def iter_shortcuts(self):
"""Iterate over keyboard shortcuts."""
for context_name, keystr in self._user_config.items('shortcuts'):
+ if context_name == 'enable':
+ continue
+
if 'additional_configuration' not in context_name:
context, name = context_name.split('/', 1)
yield context, name, keystr
diff --git a/spyder/plugins/appearance/plugin.py b/spyder/plugins/appearance/plugin.py
index 6232032dbb1..3e09c5908f1 100644
--- a/spyder/plugins/appearance/plugin.py
+++ b/spyder/plugins/appearance/plugin.py
@@ -39,7 +39,8 @@ class Appearance(SpyderPluginV2):
# --- SpyderPluginV2 API
# ------------------------------------------------------------------------
- def get_name(self):
+ @staticmethod
+ def get_name():
return _("Appearance")
def get_description(self):
diff --git a/spyder/plugins/application/plugin.py b/spyder/plugins/application/plugin.py
index d27c09f5bb5..ac5ba0df33c 100644
--- a/spyder/plugins/application/plugin.py
+++ b/spyder/plugins/application/plugin.py
@@ -28,7 +28,9 @@
running_under_pytest)
from spyder.plugins.application.confpage import ApplicationConfigPage
from spyder.plugins.application.container import (
- ApplicationContainer, ApplicationPluginMenus, WinUserEnvDialog)
+ ApplicationActions, ApplicationContainer, ApplicationPluginMenus,
+ WinUserEnvDialog)
+from spyder.plugins.console.api import ConsoleActions
from spyder.plugins.mainmenu.api import (
ApplicationMenus, FileMenuSections, HelpMenuSections, ToolsMenuSections)
from spyder.utils.qthelpers import add_actions
@@ -48,7 +50,8 @@ class Application(SpyderPluginV2):
CONF_WIDGET_CLASS = ApplicationConfigPage
CAN_BE_DISABLED = False
- def get_name(self):
+ @staticmethod
+ def get_name():
return _('Application')
def get_icon(self):
@@ -101,6 +104,28 @@ def on_editor_available(self):
self.get_container().sig_load_log_file.connect(editor.load)
# -------------------------- PLUGIN TEARDOWN ------------------------------
+ @on_plugin_teardown(plugin=Plugins.Preferences)
+ def on_preferences_teardown(self):
+ preferences = self.get_plugin(Plugins.Preferences)
+ preferences.deregister_plugin_preferences(self)
+
+ @on_plugin_teardown(plugin=Plugins.Editor)
+ def on_editor_teardown(self):
+ editor = self.get_plugin(Plugins.Editor)
+ self.get_container().sig_load_log_file.disconnect(editor.load)
+
+ @on_plugin_teardown(plugin=Plugins.Console)
+ def on_console_teardown(self):
+ if self.is_plugin_available(Plugins.MainMenu):
+ self.report_action.setVisible(False)
+
+ @on_plugin_teardown(plugin=Plugins.MainMenu)
+ def on_main_menu_teardown(self):
+ self._depopulate_file_menu()
+ self._depopulate_tools_menu()
+ self._depopulate_help_menu()
+ self.report_action.setVisible(False)
+
def on_close(self, _unused=True):
self.get_container().on_close()
@@ -206,6 +231,60 @@ def _populate_help_menu_about_section(self):
def _window(self):
return self.main.window()
+ def _depopulate_help_menu(self):
+ self._depopulate_help_menu_documentation_section()
+ self._depopulate_help_menu_support_section()
+ self._depopulate_help_menu_about_section()
+
+ def _depopulate_help_menu_documentation_section(self):
+ mainmenu = self.get_plugin(Plugins.MainMenu)
+ for documentation_action in [
+ ApplicationActions.SpyderDocumentationAction,
+ ApplicationActions.SpyderDocumentationVideoAction]:
+ mainmenu.remove_item_from_application_menu(
+ documentation_action,
+ menu_id=ApplicationMenus.Help)
+
+ def _depopulate_help_menu_support_section(self):
+ """Remove Spyder base support actions from the Help main menu."""
+ mainmenu = self.get_plugin(Plugins.MainMenu)
+ for support_action in [
+ ApplicationActions.SpyderTroubleshootingAction,
+ ConsoleActions.SpyderReportAction,
+ ApplicationActions.SpyderDependenciesAction,
+ ApplicationActions.SpyderCheckUpdatesAction,
+ ApplicationActions.SpyderSupportAction]:
+ mainmenu.remove_item_from_application_menu(
+ support_action,
+ menu_id=ApplicationMenus.Help)
+
+ def _depopulate_help_menu_about_section(self):
+ mainmenu = self.get_plugin(Plugins.MainMenu)
+ mainmenu.remove_item_from_application_menu(
+ ApplicationActions.SpyderAbout,
+ menu_id=ApplicationMenus.Help)
+
+ def _depopulate_file_menu(self):
+ mainmenu = self.get_plugin(Plugins.MainMenu)
+ for action_id in [ApplicationActions.SpyderRestart,
+ ApplicationActions.SpyderRestartDebug]:
+ mainmenu.remove_item_from_application_menu(
+ action_id,
+ menu_id=ApplicationMenus.File)
+
+ def _depopulate_tools_menu(self):
+ """Add base actions and menus to the Tools menu."""
+ mainmenu = self.get_plugin(Plugins.MainMenu)
+ if WinUserEnvDialog is not None:
+ mainmenu.remove_item_from_application_menu(
+ ApplicationActions.SpyderWindowsEnvVariables,
+ menu_id=ApplicationMenus.Tools)
+
+ if get_debug_level() >= 2:
+ mainmenu.remove_item_from_application_menu(
+ ApplicationPluginMenus.DebugLogsMenu,
+ menu_id=ApplicationMenus.Tools)
+
# ---- Public API
# ------------------------------------------------------------------------
def get_application_context_menu(self, parent=None):
@@ -248,7 +327,7 @@ def apply_settings(self):
self._main.apply_settings()
@Slot()
- def restart(self):
+ def restart(self, reset=False, close_immediately=False):
"""
Quit and Restart Spyder application.
@@ -304,7 +383,7 @@ def restart(self):
command = command.format(python, restart_script)
try:
- if self.main.closing(True):
+ if self.main.closing(True, close_immediately=close_immediately):
subprocess.Popen(command, shell=shell, env=env,
startupinfo=startupinfo)
console.quit()
diff --git a/spyder/plugins/breakpoints/plugin.py b/spyder/plugins/breakpoints/plugin.py
index 2c7a20a1146..66ec9b8d0b4 100644
--- a/spyder/plugins/breakpoints/plugin.py
+++ b/spyder/plugins/breakpoints/plugin.py
@@ -18,7 +18,8 @@
# Local imports
from spyder.api.plugins import Plugins, SpyderDockablePlugin
-from spyder.api.plugin_registration.decorators import on_plugin_available
+from spyder.api.plugin_registration.decorators import (
+ on_plugin_available, on_plugin_teardown)
from spyder.api.translations import get_translation
from spyder.plugins.breakpoints.widgets.main_widget import BreakpointWidget
from spyder.plugins.mainmenu.api import ApplicationMenus
@@ -88,7 +89,8 @@ class Breakpoints(SpyderDockablePlugin):
# --- SpyderDockablePlugin API
# ------------------------------------------------------------------------
- def get_name(self):
+ @staticmethod
+ def get_name():
return _("Breakpoints")
def get_description(self):
@@ -140,6 +142,29 @@ def on_main_menu_available(self):
mainmenu.add_item_to_application_menu(
list_action, menu_id=ApplicationMenus.Debug)
+ @on_plugin_teardown(plugin=Plugins.Editor)
+ def on_editor_teardown(self):
+ widget = self.get_widget()
+ editor = self.get_plugin(Plugins.Editor)
+ list_action = self.get_action(BreakpointsActions.ListBreakpoints)
+
+ editor.breakpoints_saved.disconnect(self.set_data)
+ widget.sig_clear_all_breakpoints_requested.disconnect(
+ editor.clear_all_breakpoints)
+ widget.sig_clear_breakpoint_requested.disconnect(
+ editor.clear_breakpoint)
+ widget.sig_edit_goto_requested.disconnect(editor.load)
+ widget.sig_conditional_breakpoint_requested.disconnect(
+ editor.set_or_edit_conditional_breakpoint)
+
+ editor.pythonfile_dependent_actions.remove(list_action)
+
+ @on_plugin_teardown(plugin=Plugins.MainMenu)
+ def on_main_menu_teardown(self):
+ mainmenu = self.get_plugin(Plugins.MainMenu)
+ mainmenu.remove_item_from_application_menu(
+ BreakpointsActions.ListBreakpoints, menu_id=ApplicationMenus.Debug)
+
# --- Private API
# ------------------------------------------------------------------------
def _load_data(self):
diff --git a/spyder/plugins/completion/api.py b/spyder/plugins/completion/api.py
index 4e2b01b8bba..2d52fcda6cf 100644
--- a/spyder/plugins/completion/api.py
+++ b/spyder/plugins/completion/api.py
@@ -1275,6 +1275,35 @@ def create_action(self, name, text, icon=None, icon_text='', tip=None,
shortcut_context=shortcut_context, context=context,
initial=initial, register_shortcut=register_shortcut)
+ def get_action(self, name, context=None, plugin=None):
+ """
+ Return an action by name, context and plugin.
+
+ Parameters
+ ----------
+ name: str
+ Name of the action to retrieve.
+ context: Optional[str]
+ Widget or context identifier under which the action was stored.
+ If None, then `CONTEXT_NAME` is used instead
+ plugin: Optional[str]
+ Name of the plugin where the action was defined. If None, then
+ `PLUGIN_NAME` is used.
+
+ Returns
+ -------
+ action: SpyderAction
+ The corresponding action stored under the given `name`, `context`
+ and `plugin`.
+
+ Raises
+ ------
+ KeyError
+ If either of `name`, `context` or `plugin` keys do not exist in the
+ toolbar registry.
+ """
+ return self.main.get_action(name, context=context, plugin=plugin)
+
def create_application_menu(self, menu_id, title, dynamic=True):
"""
Create a Spyder application menu.
@@ -1361,3 +1390,17 @@ def add_item_to_application_menu(self, item, menu_id=None,
self.main.add_item_to_application_menu(
item, menu_id=menu_id, section=section,
before=before, before_section=before_section)
+
+ def remove_item_from_application_menu(self, item_id: str,
+ menu_id: Optional[str] = None):
+ """
+ Remove action or widget from given application menu by id.
+
+ Parameters
+ ----------
+ item_id: str
+ The item identifier to remove from the given menu.
+ menu_id: str or None
+ The application menu unique string identifier.
+ """
+ self.main.remove_item_from_application_menu(item_id, menu_id=menu_id)
diff --git a/spyder/plugins/completion/plugin.py b/spyder/plugins/completion/plugin.py
index 0587dc43d74..8e7cb79834a 100644
--- a/spyder/plugins/completion/plugin.py
+++ b/spyder/plugins/completion/plugin.py
@@ -27,7 +27,8 @@
# Local imports
from spyder.config.manager import CONF
from spyder.api.plugins import SpyderPluginV2, Plugins
-from spyder.api.plugin_registration.decorators import on_plugin_available
+from spyder.api.plugin_registration.decorators import (
+ on_plugin_available, on_plugin_teardown)
from spyder.config.base import _, running_under_pytest
from spyder.config.user import NoDefault
from spyder.plugins.completion.api import (CompletionRequestTypes,
@@ -253,7 +254,8 @@ def __init__(self, parent, configuration=None):
self.ADDITIONAL_CONF_TABS = {'completions': conf_tabs}
# ---------------- Public Spyder API required methods ---------------------
- def get_name(self) -> str:
+ @staticmethod
+ def get_name() -> str:
return _('Completion and linting')
def get_description(self) -> str:
@@ -310,7 +312,43 @@ def on_mainmenu_available(self):
for args, kwargs in self.items_to_add_to_application_menus:
main_menu.add_item_to_application_menu(*args, **kwargs)
- def unregister(self):
+ @on_plugin_teardown(plugin=Plugins.Preferences)
+ def on_preferences_teardown(self):
+ preferences = self.get_plugin(Plugins.Preferences)
+ preferences.deregister_plugin_preferences(self)
+
+ @on_plugin_teardown(plugin=Plugins.StatusBar)
+ def on_statusbar_teardown(self):
+ container = self.get_container()
+ self.statusbar = self.get_plugin(Plugins.StatusBar)
+ for sb in container.all_statusbar_widgets():
+ self.statusbar.remove_status_widget(sb.ID)
+
+ @on_plugin_teardown(plugin=Plugins.MainMenu)
+ def on_mainmenu_teardown(self):
+ main_menu = self.get_plugin(Plugins.MainMenu)
+ signature = inspect.signature(main_menu.add_item_to_application_menu)
+
+ for args, kwargs in self.application_menus_to_create:
+ menu_id = args[0]
+ main_menu.remove_application_menu(menu_id)
+
+ for args, kwargs in self.items_to_add_to_application_menus:
+ binding = signature.bind(*args, **kwargs)
+ binding.apply_defaults()
+
+ item = binding.arguments['item']
+ menu_id = binding.arguments['menu_id']
+ item_id = None
+ if hasattr(item, 'action_id'):
+ item_id = item.action_id
+ elif hasattr(item, 'menu_id'):
+ item_id = item.menu_id
+ if item_id is not None:
+ main_menu.remove_item_from_application_menu(
+ item_id, menu_id=menu_id)
+
+ def stop_all_providers(self):
"""Stop all running completion providers."""
for provider_name in self.providers:
provider_info = self.providers[provider_name]
@@ -318,6 +356,17 @@ def unregister(self):
# TODO: Remove status bar widgets
provider_info['instance'].shutdown()
+ def can_close(self) -> bool:
+ """Check if any provider has any pending task."""
+ can_close = False
+ for provider_name in self.providers:
+ provider_info = self.providers[provider_name]
+ if provider_info['status'] == self.RUNNING:
+ provider = provider_info['instance']
+ provider_can_close = provider.can_close()
+ can_close |= provider_can_close
+ return can_close
+
def on_close(self, cancelable=False) -> bool:
"""Check if any provider has any pending task before closing."""
can_close = False
@@ -339,6 +388,8 @@ def after_configuration_update(self, options: List[Union[tuple, str]]):
provider tabs.
"""
providers_to_update = set({})
+ options = [x[1] if isinstance(x, tuple) and
+ len(x) == 2 and x[0] is None else x for x in options]
for option in options:
if option == 'completions_wait_for_ms':
self.wait_for_ms = self.get_conf(
@@ -886,6 +937,10 @@ def create_action(self, *args, **kwargs):
kwargs['parent'] = container
return container.create_action(*args, **kwargs)
+ def get_action(self, *args, **kwargs):
+ container = self.get_container()
+ return container.get_action(*args, **kwargs)
+
def get_application_menu(self, *args, **kwargs):
# TODO: Check if this method makes sense with the new plugin
# registration mechanism.
@@ -907,6 +962,11 @@ def create_menu(self, *args, **kwargs):
def add_item_to_application_menu(self, *args, **kwargs):
self.items_to_add_to_application_menus.append((args, kwargs))
+ def remove_item_from_application_menu(self, *args, **kwargs):
+ main_menu = self.get_plugin(Plugins.MainMenu)
+ if main_menu:
+ main_menu.remove_item_from_application_menu(*args, **kwargs)
+
def add_item_to_menu(self, *args, **kwargs):
container = self.get_container()
container.add_item_to_menu(*args, **kwargs)
diff --git a/spyder/plugins/completion/providers/kite/provider.py b/spyder/plugins/completion/providers/kite/provider.py
index 3c2f96bfa26..2118f57952e 100644
--- a/spyder/plugins/completion/providers/kite/provider.py
+++ b/spyder/plugins/completion/providers/kite/provider.py
@@ -127,6 +127,14 @@ def start(self):
self.client.start()
def shutdown(self):
+ try:
+ self.remove_item_from_application_menu(
+ KiteProviderActions.Installation,
+ menu_id=ApplicationMenus.Tools)
+ except KeyError:
+ # Action does not exist
+ pass
+
self.client.stop()
if self.kite_process is not None:
self.kite_process.kill()
diff --git a/spyder/plugins/completion/tests/conftest.py b/spyder/plugins/completion/tests/conftest.py
index 2e22ee99725..354c0bdb076 100644
--- a/spyder/plugins/completion/tests/conftest.py
+++ b/spyder/plugins/completion/tests/conftest.py
@@ -188,7 +188,7 @@ def wait_until_all_started():
def teardown():
os.environ['SPY_TEST_USE_INTROSPECTION'] = 'False'
- completion_plugin.unregister()
+ completion_plugin.stop_all_providers()
request.addfinalizer(teardown)
return completion_plugin, capabilities
diff --git a/spyder/plugins/console/plugin.py b/spyder/plugins/console/plugin.py
index f1f105f94a1..32c9d2a6443 100644
--- a/spyder/plugins/console/plugin.py
+++ b/spyder/plugins/console/plugin.py
@@ -17,9 +17,11 @@
# Local imports
from spyder.api.plugins import Plugins, SpyderDockablePlugin
-from spyder.api.plugin_registration.decorators import on_plugin_available
+from spyder.api.plugin_registration.decorators import (
+ on_plugin_available, on_plugin_teardown)
from spyder.api.translations import get_translation
-from spyder.plugins.console.widgets.main_widget import ConsoleWidget
+from spyder.plugins.console.widgets.main_widget import (
+ ConsoleWidget, ConsoleWidgetActions)
from spyder.plugins.mainmenu.api import ApplicationMenus, FileMenuSections
# Localization
@@ -74,7 +76,8 @@ class Console(SpyderDockablePlugin):
# --- SpyderDockablePlugin API
# ------------------------------------------------------------------------
- def get_name(self):
+ @staticmethod
+ def get_name():
return _('Internal console')
def get_icon(self):
@@ -122,6 +125,14 @@ def on_main_menu_available(self):
menu_id=ApplicationMenus.File,
section=FileMenuSections.Restart)
+ @on_plugin_teardown(plugin=Plugins.MainMenu)
+ def on_main_menu_teardown(self):
+ widget = self.get_widget()
+ mainmenu = self.get_plugin(Plugins.MainMenu)
+ mainmenu.remove_item_from_application_menu(
+ ConsoleWidgetActions.Quit,
+ menu_id=ApplicationMenus.File)
+
def update_font(self):
font = self.get_font()
self.get_widget().set_font(font)
diff --git a/spyder/plugins/editor/plugin.py b/spyder/plugins/editor/plugin.py
index 24d31f52258..b594cba43a9 100644
--- a/spyder/plugins/editor/plugin.py
+++ b/spyder/plugins/editor/plugin.py
@@ -416,8 +416,11 @@ def _rpc_call(self, method, args, kwargs):
meth(*args, **kwargs)
#------ SpyderPluginWidget API ---------------------------------------------
- def get_plugin_title(self):
+ @staticmethod
+ def get_plugin_title():
"""Return widget title"""
+ # TODO: This is a temporary measure to get the title of this plugin
+ # without creating an instance
title = _('Editor')
return title
@@ -1069,6 +1072,7 @@ def get_plugin_actions(self):
find_previous_action,
replace_action,
gotoline_action]
+
self.main.search_toolbar_actions = [find_action,
find_next_action,
replace_action]
@@ -1081,6 +1085,10 @@ def get_plugin_actions(self):
self.text_lowercase_action]
# ---- Search menu/toolbar construction ----
+ if not hasattr(self.main, 'search_menu_actions'):
+ # This list will not exist in the fast tests.
+ self.main.search_menu_actions = []
+
self.main.search_menu_actions = (
search_menu_actions + self.main.search_menu_actions)
diff --git a/spyder/plugins/explorer/plugin.py b/spyder/plugins/explorer/plugin.py
index 3ec04a6fe36..c643e360bc3 100644
--- a/spyder/plugins/explorer/plugin.py
+++ b/spyder/plugins/explorer/plugin.py
@@ -20,7 +20,8 @@
# Local imports
from spyder.api.translations import get_translation
from spyder.api.plugins import SpyderDockablePlugin, Plugins
-from spyder.api.plugin_registration.decorators import on_plugin_available
+from spyder.api.plugin_registration.decorators import (
+ on_plugin_available, on_plugin_teardown)
from spyder.plugins.explorer.widgets.main_widget import ExplorerWidget
from spyder.plugins.explorer.confpage import ExplorerConfigPage
@@ -152,7 +153,8 @@ class Explorer(SpyderDockablePlugin):
# ---- SpyderDockablePlugin API
# ------------------------------------------------------------------------
- def get_name(self):
+ @staticmethod
+ def get_name():
"""Return widget title"""
return _("Files")
@@ -210,6 +212,30 @@ def on_ipython_console_available(self):
ipyconsole.run_script(fname, osp.dirname(fname), '', False,
False, False, True, False))
+ @on_plugin_teardown(plugin=Plugins.Editor)
+ def on_editor_teardown(self):
+ editor = self.get_plugin(Plugins.Editor)
+
+ editor.sig_dir_opened.disconnect(self.chdir)
+ self.sig_file_created.disconnect()
+ self.sig_file_removed.disconnect(editor.removed)
+ self.sig_file_renamed.disconnect(editor.renamed)
+ self.sig_folder_removed.disconnect(editor.removed_tree)
+ self.sig_folder_renamed.disconnect(editor.renamed_tree)
+ self.sig_module_created.disconnect(editor.new)
+ self.sig_open_file_requested.disconnect(editor.load)
+
+ @on_plugin_teardown(plugin=Plugins.Preferences)
+ def on_preferences_teardown(self):
+ preferences = self.get_plugin(Plugins.Preferences)
+ preferences.deregister_plugin_preferences(self)
+
+ @on_plugin_teardown(plugin=Plugins.IPythonConsole)
+ def on_ipython_console_teardown(self):
+ ipyconsole = self.get_plugin(Plugins.IPythonConsole)
+ self.sig_interpreter_opened.disconnect(
+ ipyconsole.create_client_from_path)
+ self.sig_run_requested.disconnect()
# ---- Public API
# ------------------------------------------------------------------------
diff --git a/spyder/plugins/findinfiles/plugin.py b/spyder/plugins/findinfiles/plugin.py
index 2b798a8b2b2..2615203c486 100644
--- a/spyder/plugins/findinfiles/plugin.py
+++ b/spyder/plugins/findinfiles/plugin.py
@@ -13,7 +13,8 @@
# Local imports
from spyder.api.plugins import Plugins, SpyderDockablePlugin
-from spyder.api.plugin_registration.decorators import on_plugin_available
+from spyder.api.plugin_registration.decorators import (
+ on_plugin_available, on_plugin_teardown)
from spyder.api.translations import get_translation
from spyder.plugins.findinfiles.widgets import FindInFilesWidget
from spyder.plugins.mainmenu.api import ApplicationMenus
@@ -45,7 +46,8 @@ class FindInFiles(SpyderDockablePlugin):
# --- SpyderDocakblePlugin API
# ------------------------------------------------------------------------
- def get_name(self):
+ @staticmethod
+ def get_name():
return _("Find")
def get_description(self):
@@ -91,6 +93,29 @@ def on_main_menu_available(self):
menu_id=ApplicationMenus.Search,
)
+ @on_plugin_teardown(plugin=Plugins.Editor)
+ def on_editor_teardown(self):
+ widget = self.get_widget()
+ editor = self.get_plugin(Plugins.Editor)
+ widget.sig_edit_goto_requested.disconnect()
+ editor.sig_file_opened_closed_or_updated.disconnect(
+ self.set_current_opened_file)
+
+ @on_plugin_teardown(plugin=Plugins.Projects)
+ def on_projects_teardon_plugin_teardown(self):
+ projects = self.get_plugin(Plugins.Projects)
+ projects.sig_project_loaded.disconnect(self.set_project_path)
+ projects.sig_project_closed.disconnect(self.unset_project_path)
+
+ @on_plugin_teardown(plugin=Plugins.MainMenu)
+ def on_main_menu_teardown(self):
+ mainmenu = self.get_plugin(Plugins.MainMenu)
+
+ mainmenu.remove_item_from_application_menu(
+ FindInFilesActions.FindInFiles,
+ menu_id=ApplicationMenus.Search,
+ )
+
def on_close(self, cancelable=False):
self.get_widget()._update_options()
self.get_widget()._stop_and_reset_thread(ignore_results=True)
diff --git a/spyder/plugins/help/plugin.py b/spyder/plugins/help/plugin.py
index c2411f0e0bc..62b64527acd 100644
--- a/spyder/plugins/help/plugin.py
+++ b/spyder/plugins/help/plugin.py
@@ -17,7 +17,8 @@
# Local imports
from spyder import __docs_url__, __forum_url__, __trouble_url__
from spyder.api.plugins import Plugins, SpyderDockablePlugin
-from spyder.api.plugin_registration.decorators import on_plugin_available
+from spyder.api.plugin_registration.decorators import (
+ on_plugin_available, on_plugin_teardown)
from spyder.api.translations import get_translation
from spyder.config.base import get_conf_path
from spyder.config.fonts import DEFAULT_SMALL_DELTA
@@ -62,7 +63,8 @@ class Help(SpyderDockablePlugin):
# --- SpyderDocakblePlugin API
# -----------------------------------------------------------------------
- def get_name(self):
+ @staticmethod
+ def get_name():
return _('Help')
def get_description(self):
@@ -126,8 +128,8 @@ def on_shortcuts_available(self):
shortcuts = self.get_plugin(Plugins.Shortcuts)
# See: spyder-ide/spyder#6992
- shortcuts.sig_shortcuts_updated.connect(
- lambda: self.show_intro_message())
+ self._show_intro_message = lambda: self.show_intro_message()
+ shortcuts.sig_shortcuts_updated.connect(self._show_intro_message)
if self.is_plugin_available(Plugins.MainMenu):
self._setup_menus()
@@ -140,6 +142,46 @@ def on_main_menu_available(self):
else:
self._setup_menus()
+ @on_plugin_teardown(plugin=Plugins.Console)
+ def on_console_teardown(self):
+ widget = self.get_widget()
+ internal_console = self.get_plugin(Plugins.Console)
+ internal_console.sig_help_requested.disconnect(self.set_object_text)
+ widget.set_internal_console(None)
+
+ @on_plugin_teardown(plugin=Plugins.Editor)
+ def on_editor_teardown(self):
+ editor = self.get_plugin(Plugins.Editor)
+ editor.sig_help_requested.disconnect(self.set_editor_doc)
+
+ @on_plugin_teardown(plugin=Plugins.IPythonConsole)
+ def on_ipython_console_teardown(self):
+ ipyconsole = self.get_plugin(Plugins.IPythonConsole)
+
+ ipyconsole.sig_shellwidget_changed.disconnect(self.set_shellwidget)
+ ipyconsole.sig_shellwidget_created.disconnect(
+ self.set_shellwidget)
+ ipyconsole.sig_render_plain_text_requested.disconnect(
+ self.show_plain_text)
+ ipyconsole.sig_render_rich_text_requested.disconnect(
+ self.show_rich_text)
+
+ ipyconsole.sig_help_requested.disconnect(self.set_object_text)
+
+ @on_plugin_teardown(plugin=Plugins.Preferences)
+ def on_preferences_teardown(self):
+ preferences = self.get_plugin(Plugins.Preferences)
+ preferences.deregister_plugin_preferences(self)
+
+ @on_plugin_teardown(plugin=Plugins.Shortcuts)
+ def on_shortcuts_teardown(self):
+ shortcuts = self.get_plugin(Plugins.Shortcuts)
+ shortcuts.sig_shortcuts_updated.disconnect(self._show_intro_message)
+
+ @on_plugin_teardown(plugin=Plugins.MainMenu)
+ def on_main_menu_teardown(self):
+ self._remove_menus()
+
def update_font(self):
color_scheme = self.get_color_scheme()
font = self.get_font()
@@ -189,6 +231,13 @@ def _setup_menus(self):
before=shortcuts_summary_action,
before_section=HelpMenuSections.Support)
+ def _remove_menus(self):
+ from spyder.plugins.mainmenu.api import ApplicationMenus
+ mainmenu = self.get_plugin(Plugins.MainMenu)
+ mainmenu.remove_item_from_application_menu(
+ HelpActions.ShowSpyderTutorialAction,
+ menu_id=ApplicationMenus.Help)
+
# --- Public API
# ------------------------------------------------------------------------
def set_shellwidget(self, shellwidget):
diff --git a/spyder/plugins/help/widgets.py b/spyder/plugins/help/widgets.py
index 12bc411b5d8..c3fa1cef088 100644
--- a/spyder/plugins/help/widgets.py
+++ b/spyder/plugins/help/widgets.py
@@ -1143,4 +1143,5 @@ def set_internal_console(self, console):
Console plugin.
"""
self.internal_console = console
- self.internal_shell = console.get_widget().shell
+ if self.internal_console is not None:
+ self.internal_shell = console.get_widget().shell
diff --git a/spyder/plugins/history/plugin.py b/spyder/plugins/history/plugin.py
index 40173f18661..b8d50b78981 100644
--- a/spyder/plugins/history/plugin.py
+++ b/spyder/plugins/history/plugin.py
@@ -13,7 +13,8 @@
# Local imports
from spyder.api.plugins import Plugins, SpyderDockablePlugin
-from spyder.api.plugin_registration.decorators import on_plugin_available
+from spyder.api.plugin_registration.decorators import (
+ on_plugin_available, on_plugin_teardown)
from spyder.api.translations import get_translation
from spyder.plugins.history.confpage import HistoryConfigPage
from spyder.plugins.history.widgets import HistoryWidget
@@ -45,7 +46,8 @@ class HistoryLog(SpyderDockablePlugin):
# --- SpyderDockablePlugin API
# ------------------------------------------------------------------------
- def get_name(self):
+ @staticmethod
+ def get_name():
return _('History')
def get_description(self):
@@ -68,6 +70,16 @@ def on_console_available(self):
console = self.get_plugin(Plugins.Console)
console.sig_refreshed.connect(self.refresh)
+ @on_plugin_teardown(plugin=Plugins.Preferences)
+ def on_preferences_teardown(self):
+ preferences = self.get_plugin(Plugins.Preferences)
+ preferences.deregister_plugin_preferences(self)
+
+ @on_plugin_teardown(plugin=Plugins.Console)
+ def on_console_teardown(self):
+ console = self.get_plugin(Plugins.Console)
+ console.sig_refreshed.disconnect(self.refresh)
+
def update_font(self):
color_scheme = self.get_color_scheme()
font = self.get_font()
diff --git a/spyder/plugins/ipythonconsole/plugin.py b/spyder/plugins/ipythonconsole/plugin.py
index 6a7e3503c7b..5eaa3cd5bb6 100644
--- a/spyder/plugins/ipythonconsole/plugin.py
+++ b/spyder/plugins/ipythonconsole/plugin.py
@@ -520,8 +520,11 @@ def toggle_view(self, checked):
self.dockwidget.hide()
#------ SpyderPluginWidget API --------------------------------------------
- def get_plugin_title(self):
+ @staticmethod
+ def get_plugin_title():
"""Return widget title"""
+ # TODO: This is a temporary measure to get the title of this plugin
+ # without creating an instance
return _('IPython console')
def get_plugin_icon(self):
@@ -760,7 +763,7 @@ def register_plugin(self):
self.main.sig_pythonpath_changed.connect(self.update_path)
# Show history file if no console is visible
- if not self._isvisible and self.main.historylog:
+ if not self._isvisible and hasattr(self.main, 'historylog'):
self.main.historylog.add_history(get_conf_path('history.py'))
#------ Public API (for clients) ------------------------------------------
diff --git a/spyder/plugins/layout/container.py b/spyder/plugins/layout/container.py
index e25f055788e..18b2c6f47b4 100644
--- a/spyder/plugins/layout/container.py
+++ b/spyder/plugins/layout/container.py
@@ -58,6 +58,11 @@ class LayoutContainerActions:
LockDockwidgetsAndToolbars = 'Lock unlock panes'
+class LayoutPluginMenus:
+ PluginsMenu = "plugins_menu"
+ LayoutsMenu = 'layouts_menu'
+
+
class LayoutContainer(PluginMainContainer):
"""
Plugin container class that handles the Spyder quick layouts functionality.
@@ -155,10 +160,10 @@ def setup(self):
# Layouts menu
self._layouts_menu = self.create_menu(
- "layouts_menu", _("Window layouts"))
+ LayoutPluginMenus.LayoutsMenu, _("Window layouts"))
self._plugins_menu = self.create_menu(
- "plugins_menu", _("Panes"))
+ LayoutPluginMenus.PluginsMenu, _("Panes"))
self._plugins_menu.setObjectName('checkbox-padding')
def update_actions(self):
diff --git a/spyder/plugins/layout/plugin.py b/spyder/plugins/layout/plugin.py
index d48b4d5b1e0..c2abf8e7066 100644
--- a/spyder/plugins/layout/plugin.py
+++ b/spyder/plugins/layout/plugin.py
@@ -18,11 +18,13 @@
# Local imports
from spyder.api.exceptions import SpyderAPIError
from spyder.api.plugins import Plugins, SpyderPluginV2
-from spyder.api.plugin_registration.decorators import on_plugin_available
+from spyder.api.plugin_registration.decorators import (
+ on_plugin_available, on_plugin_teardown)
from spyder.api.translations import get_translation
from spyder.api.utils import get_class_values
from spyder.plugins.mainmenu.api import ApplicationMenus, ViewMenuSections
-from spyder.plugins.layout.container import LayoutContainer
+from spyder.plugins.layout.container import (
+ LayoutContainer, LayoutContainerActions, LayoutPluginMenus)
from spyder.plugins.layout.layouts import (DefaultLayouts,
HorizontalSplitLayout,
MatlabLayout, RLayout,
@@ -70,7 +72,8 @@ class Layout(SpyderPluginV2):
# --- SpyderDockablePlugin API
# ------------------------------------------------------------------------
- def get_name(self):
+ @staticmethod
+ def get_name():
return _("Layout")
def get_description(self):
@@ -146,6 +149,43 @@ def on_toolbar_available(self):
before=PreferencesActions.Show
)
+ @on_plugin_teardown(plugin=Plugins.MainMenu)
+ def on_main_menu_teardown(self):
+ mainmenu = self.get_plugin(Plugins.MainMenu)
+ # Remove Panes related actions from the View application menu
+ panes_items = [
+ LayoutPluginMenus.PluginsMenu,
+ LayoutContainerActions.LockDockwidgetsAndToolbars,
+ LayoutContainerActions.CloseCurrentDockwidget,
+ LayoutContainerActions.MaximizeCurrentDockwidget]
+ for panes_item in panes_items:
+ mainmenu.remove_item_from_application_menu(
+ panes_item,
+ menu_id=ApplicationMenus.View)
+ # Remove layouts menu from the View application menu
+ layout_items = [
+ LayoutPluginMenus.LayoutsMenu,
+ LayoutContainerActions.NextLayout,
+ LayoutContainerActions.PreviousLayout]
+ for layout_item in layout_items:
+ mainmenu.remove_item_from_application_menu(
+ layout_item,
+ menu_id=ApplicationMenus.View)
+ # Remove fullscreen action from the View application menu
+ mainmenu.remove_item_from_application_menu(
+ LayoutContainerActions.Fullscreen,
+ menu_id=ApplicationMenus.View)
+
+ @on_plugin_teardown(plugin=Plugins.Toolbar)
+ def on_toolbar_teardown(self):
+ toolbars = self.get_plugin(Plugins.Toolbar)
+
+ # Remove actions from the Main application toolbar
+ toolbars.remove_item_from_application_toolbar(
+ LayoutContainerActions.MaximizeCurrentDockwidget,
+ toolbar_id=ApplicationToolbars.Main
+ )
+
def before_mainwindow_visible(self):
# Update layout menu
self.update_layout_menu_actions()
diff --git a/spyder/plugins/maininterpreter/plugin.py b/spyder/plugins/maininterpreter/plugin.py
index 8fb85779c3b..91ad3be6949 100644
--- a/spyder/plugins/maininterpreter/plugin.py
+++ b/spyder/plugins/maininterpreter/plugin.py
@@ -17,7 +17,8 @@
# Local imports
from spyder.api.plugins import Plugins, SpyderPluginV2
-from spyder.api.plugin_registration.decorators import on_plugin_available
+from spyder.api.plugin_registration.decorators import (
+ on_plugin_available, on_plugin_teardown)
from spyder.api.translations import get_translation
from spyder.plugins.maininterpreter.confpage import MainInterpreterConfigPage
from spyder.plugins.maininterpreter.container import MainInterpreterContainer
@@ -41,7 +42,8 @@ class MainInterpreter(SpyderPluginV2):
CONF_FILE = False
# ---- SpyderPluginV2 API
- def get_name(self):
+ @staticmethod
+ def get_name():
return _("Python interpreter")
def get_description(self):
@@ -87,8 +89,19 @@ def on_preferences_available(self):
def on_statusbar_available(self):
# Add status widget
statusbar = self.get_plugin(Plugins.StatusBar)
- if statusbar:
- statusbar.add_status_widget(self.interpreter_status)
+ statusbar.add_status_widget(self.interpreter_status)
+
+ @on_plugin_teardown(plugin=Plugins.Preferences)
+ def on_preferences_teardown(self):
+ # Deregister conf page
+ preferences = self.get_plugin(Plugins.Preferences)
+ preferences.deregister_plugin_preferences(self)
+
+ @on_plugin_teardown(plugin=Plugins.StatusBar)
+ def on_statusbar_teardown(self):
+ # Add status widget
+ statusbar = self.get_plugin(Plugins.StatusBar)
+ statusbar.remove_status_widget(self.interpreter_status.ID)
# ---- Public API
def get_interpreter(self):
diff --git a/spyder/plugins/mainmenu/plugin.py b/spyder/plugins/mainmenu/plugin.py
index 672fff513b1..9b97421645a 100644
--- a/spyder/plugins/mainmenu/plugin.py
+++ b/spyder/plugins/mainmenu/plugin.py
@@ -40,7 +40,8 @@ class MainMenu(SpyderPluginV2):
CONF_SECTION = NAME
CONF_FILE = False
- def get_name(self):
+ @staticmethod
+ def get_name():
return _('Main menus')
def get_icon(self):
@@ -264,6 +265,80 @@ def add_item_to_application_menu(self, item: ItemType,
menu.add_action(item, section=section, before=before,
before_section=before_section, omit_id=omit_id)
+ def remove_application_menu(self, menu_id: str):
+ """
+ Remove a Spyder application menu.
+
+ Parameters
+ ----------
+ menu_id: str
+ The menu unique identifier string.
+ """
+ if menu_id in self._APPLICATION_MENUS:
+ menu = self._APPLICATION_MENUS.pop(menu_id)
+ self.main.menuBar().removeAction(menu.menuAction())
+
+ def remove_item_from_application_menu(self, item_id: str,
+ menu_id: Optional[str] = None):
+ """
+ Remove action or widget from given application menu by id.
+
+ Parameters
+ ----------
+ item_id: str
+ The item identifier to remove from the given menu.
+ menu_id: str or None
+ The application menu unique string identifier.
+ """
+ if menu_id not in self._APPLICATION_MENUS:
+ raise SpyderAPIError('{} is not a valid menu_id'.format(menu_id))
+
+ # TODO: For now just add the item to the bottom for non-migrated menus.
+ # Temporal solution while migration is complete
+ app_menu_actions = {
+ ApplicationMenus.Edit: (
+ self._main.edit_menu_actions, self._main.edit_menu),
+ ApplicationMenus.Search: (
+ self._main.search_menu_actions, self._main.search_menu),
+ ApplicationMenus.Source: (
+ self._main.source_menu_actions, self._main.source_menu),
+ ApplicationMenus.Run: (
+ self._main.run_menu_actions, self._main.run_menu),
+ ApplicationMenus.Debug: (
+ self._main.debug_menu_actions, self._main.debug_menu),
+ }
+
+ app_menus = {
+ ApplicationMenus.Edit: self._main.edit_menu,
+ ApplicationMenus.Search: self._main.search_menu,
+ ApplicationMenus.Source: self._main.source_menu,
+ ApplicationMenus.Run: self._main.run_menu,
+ ApplicationMenus.Debug: self._main.debug_menu
+ }
+
+ menu = self.get_application_menu(menu_id)
+
+ if menu_id in app_menu_actions:
+ actions = app_menu_actions[menu_id] # type: list
+ menu = app_menus[menu_id]
+ position = None
+ for i, action in enumerate(actions):
+ this_item_id = None
+ if (isinstance(action, SpyderAction) or
+ hasattr(action, 'action_id')):
+ this_item_id = action.action_id
+ elif (isinstance(action, SpyderMenu) or
+ hasattr(action, 'menu_id')):
+ this_item_id = action.menu_id
+ if this_item_id is not None and this_item_id == item_id:
+ position = i
+ break
+ if position is not None:
+ actions.pop(position)
+ menu.remove_action(item_id)
+ else:
+ menu.remove_action(item_id)
+
def get_application_menu(self, menu_id: str) -> SpyderMenu:
"""
Return an application menu by menu unique id.
diff --git a/spyder/plugins/onlinehelp/plugin.py b/spyder/plugins/onlinehelp/plugin.py
index ba9dd218574..664c7fab4fd 100644
--- a/spyder/plugins/onlinehelp/plugin.py
+++ b/spyder/plugins/onlinehelp/plugin.py
@@ -45,7 +45,8 @@ class OnlineHelp(SpyderDockablePlugin):
# --- SpyderDockablePlugin API
# ------------------------------------------------------------------------
- def get_name(self):
+ @staticmethod
+ def get_name():
return _('Online help')
def get_description(self):
diff --git a/spyder/plugins/outlineexplorer/plugin.py b/spyder/plugins/outlineexplorer/plugin.py
index a4fb71b3f2b..3839f01e0a5 100644
--- a/spyder/plugins/outlineexplorer/plugin.py
+++ b/spyder/plugins/outlineexplorer/plugin.py
@@ -10,7 +10,8 @@
from qtpy.QtCore import Slot
# Local imports
-from spyder.api.plugin_registration.decorators import on_plugin_available
+from spyder.api.plugin_registration.decorators import (
+ on_plugin_available, on_plugin_teardown)
from spyder.api.translations import get_translation
from spyder.api.plugins import SpyderDockablePlugin, Plugins
from spyder.plugins.outlineexplorer.main_widget import OutlineExplorerWidget
@@ -30,7 +31,8 @@ class OutlineExplorer(SpyderDockablePlugin):
# ---- SpyderDockablePlugin API
# ------------------------------------------------------------------------
- def get_name(self) -> str:
+ @staticmethod
+ def get_name() -> str:
"""Return widget title."""
return _('Outline Explorer')
@@ -63,6 +65,22 @@ def on_editor_available(self):
editor.sig_open_files_finished.connect(
self.update_all_editors)
+ @on_plugin_teardown(plugin=Plugins.Completions)
+ def on_completions_teardown(self):
+ completions = self.get_plugin(Plugins.Completions)
+
+ completions.sig_language_completions_available.disconnect(
+ self.start_symbol_services)
+ completions.sig_stop_completions.disconnect(
+ self.stop_symbol_services)
+
+ @on_plugin_teardown(plugin=Plugins.Editor)
+ def on_editor_teardown(self):
+ editor = self.get_plugin(Plugins.Editor)
+
+ editor.sig_open_files_finished.disconnect(
+ self.update_all_editors)
+
#------ Public API ---------------------------------------------------------
def restore_scrollbar_position(self):
"""Restoring scrollbar position after main window is visible"""
diff --git a/spyder/plugins/plots/plugin.py b/spyder/plugins/plots/plugin.py
index 959f9724664..629de029fc0 100644
--- a/spyder/plugins/plots/plugin.py
+++ b/spyder/plugins/plots/plugin.py
@@ -13,7 +13,8 @@
# Local imports
from spyder.api.plugins import Plugins, SpyderDockablePlugin
-from spyder.api.plugin_registration.decorators import on_plugin_available
+from spyder.api.plugin_registration.decorators import (
+ on_plugin_available, on_plugin_teardown)
from spyder.api.translations import get_translation
from spyder.plugins.plots.widgets.main_widget import PlotsWidget
@@ -36,7 +37,8 @@ class Plots(SpyderDockablePlugin):
# ---- SpyderDockablePlugin API
# ------------------------------------------------------------------------
- def get_name(self):
+ @staticmethod
+ def get_name():
return _('Plots')
def get_description(self):
@@ -62,14 +64,16 @@ def on_ipython_console_available(self):
ipyconsole.sig_shellwidget_deleted.connect(
self.remove_shellwidget)
- def unregister(self):
+ @on_plugin_teardown(plugin=Plugins.IPythonConsole)
+ def on_ipython_console_teardown(self):
# Plugins
ipyconsole = self.get_plugin(Plugins.IPythonConsole)
# Signals
+ ipyconsole.sig_shellwidget_changed.disconnect(self.set_shellwidget)
ipyconsole.sig_shellwidget_created.disconnect(
self.add_shellwidget)
- ipyconsole.sig_shellwidget_deleted.connect(
+ ipyconsole.sig_shellwidget_deleted.disconnect(
self.remove_shellwidget)
# ---- Public API
diff --git a/spyder/plugins/preferences/api.py b/spyder/plugins/preferences/api.py
index 23bdaa32a3b..cd266acaa6c 100644
--- a/spyder/plugins/preferences/api.py
+++ b/spyder/plugins/preferences/api.py
@@ -10,6 +10,7 @@
# Standard library imports
import ast
+import functools
import os.path as osp
# Third party imports
@@ -211,25 +212,27 @@ def load_from_conf(self):
"""Load settings from configuration file."""
for checkbox, (sec, option, default) in list(self.checkboxes.items()):
checkbox.setChecked(self.get_option(option, default, section=sec))
- checkbox.clicked.connect(lambda _, opt=option:
- self.has_been_modified(opt))
+ checkbox.clicked.connect(lambda _, opt=option, sect=sec:
+ self.has_been_modified(sect, opt))
+ if checkbox.restart_required:
+ self.restart_options[(sec, option)] = checkbox.text()
for radiobutton, (sec, option, default) in list(
self.radiobuttons.items()):
radiobutton.setChecked(self.get_option(option, default,
section=sec))
- radiobutton.toggled.connect(lambda _foo, opt=option:
- self.has_been_modified(opt))
+ radiobutton.toggled.connect(lambda _foo, opt=option, sect=sec:
+ self.has_been_modified(sect, opt))
if radiobutton.restart_required:
- self.restart_options[option] = radiobutton.label_text
+ self.restart_options[(sec, option)] = radiobutton.label_text
for lineedit, (sec, option, default) in list(self.lineedits.items()):
data = self.get_option(option, default, section=sec)
if getattr(lineedit, 'content_type', None) == list:
data = ', '.join(data)
lineedit.setText(data)
- lineedit.textChanged.connect(lambda _, opt=option:
- self.has_been_modified(opt))
+ lineedit.textChanged.connect(lambda _, opt=option, sect=sec:
+ self.has_been_modified(sect, opt))
if lineedit.restart_required:
- self.restart_options[option] = lineedit.label_text
+ self.restart_options[(sec, option)] = lineedit.label_text
for textedit, (sec, option, default) in list(self.textedits.items()):
data = self.get_option(option, default, section=sec)
if getattr(textedit, 'content_type', None) == list:
@@ -237,14 +240,14 @@ def load_from_conf(self):
elif getattr(textedit, 'content_type', None) == dict:
data = to_text_string(data)
textedit.setPlainText(data)
- textedit.textChanged.connect(lambda opt=option:
- self.has_been_modified(opt))
+ textedit.textChanged.connect(lambda opt=option, sect=sec:
+ self.has_been_modified(sect, opt))
if textedit.restart_required:
- self.restart_options[option] = textedit.label_text
+ self.restart_options[(sec, option)] = textedit.label_text
for spinbox, (sec, option, default) in list(self.spinboxes.items()):
spinbox.setValue(self.get_option(option, default, section=sec))
- spinbox.valueChanged.connect(lambda _foo, opt=option:
- self.has_been_modified(opt))
+ spinbox.valueChanged.connect(lambda _foo, opt=option, sect=sec:
+ self.has_been_modified(sect, opt))
for combobox, (sec, option, default) in list(self.comboboxes.items()):
value = self.get_option(option, default, section=sec)
for index in range(combobox.count()):
@@ -259,10 +262,11 @@ def load_from_conf(self):
index = None
if index:
combobox.setCurrentIndex(index)
- combobox.currentIndexChanged.connect(lambda _foo, opt=option:
- self.has_been_modified(opt))
+ combobox.currentIndexChanged.connect(lambda _foo, opt=option, sect=sec:
+ self.has_been_modified(
+ sect, opt))
if combobox.restart_required:
- self.restart_options[option] = combobox.label_text
+ self.restart_options[(sec, option)] = combobox.label_text
for (fontbox, sizebox), option in list(self.fontboxes.items()):
rich_font = True if "rich" in option.lower() else False
@@ -274,9 +278,11 @@ def load_from_conf(self):
else:
property = option
fontbox.currentIndexChanged.connect(lambda _foo, opt=property:
- self.has_been_modified(opt))
+ self.has_been_modified(
+ self.CONF_SECTION, opt))
sizebox.valueChanged.connect(lambda _foo, opt=property:
- self.has_been_modified(opt))
+ self.has_been_modified(
+ self.CONF_SECTION, opt))
for clayout, (sec, option, default) in list(self.coloredits.items()):
property = to_qvariant(option)
edit = clayout.lineedit
@@ -284,13 +290,13 @@ def load_from_conf(self):
edit.setText(self.get_option(option, default, section=sec))
# QAbstractButton works differently for PySide and PyQt
if not API == 'pyside':
- btn.clicked.connect(lambda _foo, opt=option:
- self.has_been_modified(opt))
+ btn.clicked.connect(lambda _foo, opt=option, sect=sec:
+ self.has_been_modified(sect, opt))
else:
- btn.clicked.connect(lambda opt=option:
- self.has_been_modified(opt))
- edit.textChanged.connect(lambda _foo, opt=option:
- self.has_been_modified(opt))
+ btn.clicked.connect(lambda opt=option, sect=sec:
+ self.has_been_modified(sect, opt))
+ edit.textChanged.connect(lambda _foo, opt=option, sect=sec:
+ self.has_been_modified(sect, opt))
for (clayout, cb_bold, cb_italic
), (sec, option, default) in list(self.scedits.items()):
edit = clayout.lineedit
@@ -302,39 +308,39 @@ def load_from_conf(self):
cb_bold.setChecked(bold)
cb_italic.setChecked(italic)
- edit.textChanged.connect(lambda _foo, opt=option:
- self.has_been_modified(opt))
+ edit.textChanged.connect(lambda _foo, opt=option, sect=sec:
+ self.has_been_modified(sect, opt))
# QAbstractButton works differently for PySide and PyQt
if not API == 'pyside':
- btn.clicked.connect(lambda _foo, opt=option:
- self.has_been_modified(opt))
- cb_bold.clicked.connect(lambda _foo, opt=option:
- self.has_been_modified(opt))
- cb_italic.clicked.connect(lambda _foo, opt=option:
- self.has_been_modified(opt))
+ btn.clicked.connect(lambda _foo, opt=option, sect=sec:
+ self.has_been_modified(sect, opt))
+ cb_bold.clicked.connect(lambda _foo, opt=option, sect=sec:
+ self.has_been_modified(sect, opt))
+ cb_italic.clicked.connect(lambda _foo, opt=option, sect=sec:
+ self.has_been_modified(sect, opt))
else:
- btn.clicked.connect(lambda opt=option:
- self.has_been_modified(opt))
- cb_bold.clicked.connect(lambda opt=option:
- self.has_been_modified(opt))
- cb_italic.clicked.connect(lambda opt=option:
- self.has_been_modified(opt))
+ btn.clicked.connect(lambda opt=option, sect=sec:
+ self.has_been_modified(sect, opt))
+ cb_bold.clicked.connect(lambda opt=option, sect=sec:
+ self.has_been_modified(sect, opt))
+ cb_italic.clicked.connect(lambda opt=option, sect=sec:
+ self.has_been_modified(sect, opt))
def save_to_conf(self):
"""Save settings to configuration file"""
for checkbox, (sec, option, _default) in list(
self.checkboxes.items()):
- if option in self.changed_options:
+ if (sec, option) in self.changed_options:
value = checkbox.isChecked()
self.set_option(option, value, section=sec,
recursive_notification=False)
for radiobutton, (sec, option, _default) in list(
self.radiobuttons.items()):
- if option in self.changed_options:
+ if (sec, option) in self.changed_options:
self.set_option(option, radiobutton.isChecked(), section=sec,
recursive_notification=False)
for lineedit, (sec, option, _default) in list(self.lineedits.items()):
- if option in self.changed_options:
+ if (sec, option) in self.changed_options:
data = lineedit.text()
content_type = getattr(lineedit, 'content_type', None)
if content_type == list:
@@ -344,7 +350,7 @@ def save_to_conf(self):
self.set_option(option, data, section=sec,
recursive_notification=False)
for textedit, (sec, option, _default) in list(self.textedits.items()):
- if option in self.changed_options:
+ if (sec, option) in self.changed_options:
data = textedit.toPlainText()
content_type = getattr(textedit, 'content_type', None)
if content_type == dict:
@@ -359,27 +365,27 @@ def save_to_conf(self):
self.set_option(option, data, section=sec,
recursive_notification=False)
for spinbox, (sec, option, _default) in list(self.spinboxes.items()):
- if option in self.changed_options:
+ if (sec, option) in self.changed_options:
self.set_option(option, spinbox.value(), section=sec,
recursive_notification=False)
for combobox, (sec, option, _default) in list(self.comboboxes.items()):
- if option in self.changed_options:
+ if (sec, option) in self.changed_options:
data = combobox.itemData(combobox.currentIndex())
self.set_option(option, from_qvariant(data, to_text_string),
section=sec, recursive_notification=False)
for (fontbox, sizebox), option in list(self.fontboxes.items()):
- if option in self.changed_options:
+ if (self.CONF_SECTION, option) in self.changed_options:
font = fontbox.currentFont()
font.setPointSize(sizebox.value())
self.set_font(font, option)
for clayout, (sec, option, _default) in list(self.coloredits.items()):
- if option in self.changed_options:
+ if (sec, option) in self.changed_options:
self.set_option(option,
to_text_string(clayout.lineedit.text()),
section=sec, recursive_notification=False)
for (clayout, cb_bold, cb_italic), (sec, option, _default) in list(
self.scedits.items()):
- if option in self.changed_options:
+ if (sec, option) in self.changed_options:
color = to_text_string(clayout.lineedit.text())
bold = cb_bold.isChecked()
italic = cb_italic.isChecked()
@@ -387,13 +393,13 @@ def save_to_conf(self):
recursive_notification=False)
@Slot(str)
- def has_been_modified(self, option):
+ def has_been_modified(self, section, option):
self.set_modified(True)
- self.changed_options.add(option)
+ self.changed_options.add((section, option))
def create_checkbox(self, text, option, default=NoDefault,
tip=None, msg_warning=None, msg_info=None,
- msg_if_enabled=False, section=None):
+ msg_if_enabled=False, section=None, restart=False):
checkbox = QCheckBox(text)
self.checkboxes[checkbox] = (section, option, default)
if section is not None and section != self.CONF_SECTION:
@@ -410,6 +416,7 @@ def show_message(is_checked=False):
QMessageBox.information(self, self.get_name(),
msg_info, QMessageBox.Ok)
checkbox.clicked.connect(show_message)
+ checkbox.restart_required = restart
return checkbox
def create_radiobutton(self, text, option, default=NoDefault,
@@ -773,7 +780,8 @@ def create_button(self, text, callback):
btn = QPushButton(text)
btn.clicked.connect(callback)
btn.clicked.connect(
- lambda checked=False, opt='': self.has_been_modified(opt))
+ lambda checked=False, opt='': self.has_been_modified(
+ self.CONF_SECTION, opt))
return btn
def create_tab(self, *widgets):
@@ -809,7 +817,11 @@ def prompt_restart_required(self):
answer = QMessageBox.information(self, msg_title, msg,
QMessageBox.Yes | QMessageBox.No)
if answer == QMessageBox.Yes:
- self.main.application.sig_restart_requested.emit()
+ self.restart()
+
+ def restart(self):
+ """Restart Spyder."""
+ self.main.restart(close_immediately=True)
def add_tab(self, Widget):
widget = Widget(self)
diff --git a/spyder/plugins/preferences/plugin.py b/spyder/plugins/preferences/plugin.py
index b755a4f0e94..44370d58215 100644
--- a/spyder/plugins/preferences/plugin.py
+++ b/spyder/plugins/preferences/plugin.py
@@ -25,12 +25,14 @@
# Local imports
from spyder.api.plugins import Plugins, SpyderPluginV2, SpyderPlugin
-from spyder.api.plugin_registration.decorators import on_plugin_available
+from spyder.api.plugin_registration.decorators import (
+ on_plugin_available, on_plugin_teardown)
from spyder.config.base import _
from spyder.config.main import CONF_VERSION
from spyder.config.user import NoDefault
from spyder.plugins.mainmenu.api import ApplicationMenus, ToolsMenuSections
-from spyder.plugins.preferences.widgets.container import PreferencesContainer
+from spyder.plugins.preferences.widgets.container import (
+ PreferencesActions, PreferencesContainer)
from spyder.plugins.toolbar.api import ApplicationToolbars, MainToolbarSections
logger = logging.getLogger(__name__)
@@ -255,7 +257,8 @@ def open_dialog(self, prefs_dialog_size):
self.get_main())
# ---------------- Public Spyder API required methods ---------------------
- def get_name(self) -> str:
+ @staticmethod
+ def get_name() -> str:
return _('Preferences')
def get_description(self) -> str:
@@ -303,6 +306,36 @@ def on_application_available(self):
container = self.get_container()
container.sig_reset_preferences_requested.connect(self.reset)
+
+ @on_plugin_teardown(plugin=Plugins.MainMenu)
+ def on_main_menu_teardown(self):
+ container = self.get_container()
+ main_menu = self.get_plugin(Plugins.MainMenu)
+
+ main_menu.remove_item_from_application_menu(
+ PreferencesActions.Show,
+ menu_id=ApplicationMenus.Tools,
+ )
+
+ main_menu.remove_item_from_application_menu(
+ PreferencesActions.Reset,
+ menu_id=ApplicationMenus.Tools,
+ )
+
+ @on_plugin_teardown(plugin=Plugins.Toolbar)
+ def on_toolbar_teardown(self):
+ container = self.get_container()
+ toolbar = self.get_plugin(Plugins.Toolbar)
+ toolbar.remove_item_from_application_toolbar(
+ PreferencesActions.Show,
+ toolbar_id=ApplicationToolbars.Main
+ )
+
+ @on_plugin_teardown(plugin=Plugins.Application)
+ def on_application_teardown(self):
+ container = self.get_container()
+ container.sig_reset_preferences_requested.disconnect(self.reset)
+
@Slot()
def reset(self):
answer = QMessageBox.warning(self.main, _("Warning"),
@@ -314,6 +347,6 @@ def reset(self):
application = self.get_plugin(Plugins.Application)
application.sig_restart_requested.emit()
- def on_close(self, cancelable=False) -> bool:
+ def can_close(self) -> bool:
container = self.get_container()
return not container.is_dialog_open()
diff --git a/spyder/plugins/profiler/plugin.py b/spyder/plugins/profiler/plugin.py
index 884328032be..e19bc89209d 100644
--- a/spyder/plugins/profiler/plugin.py
+++ b/spyder/plugins/profiler/plugin.py
@@ -16,7 +16,8 @@
# Local imports
from spyder.api.plugins import Plugins, SpyderDockablePlugin
-from spyder.api.plugin_registration.decorators import on_plugin_available
+from spyder.api.plugin_registration.decorators import (
+ on_plugin_available, on_plugin_teardown)
from spyder.api.translations import get_translation
from spyder.plugins.mainmenu.api import ApplicationMenus
from spyder.plugins.profiler.confpage import ProfilerConfigPage
@@ -61,7 +62,8 @@ class Profiler(SpyderDockablePlugin):
# --- SpyderDockablePlugin API
# ------------------------------------------------------------------------
- def get_name(self):
+ @staticmethod
+ def get_name():
return _("Profiler")
def get_description(self):
@@ -105,6 +107,26 @@ def on_main_menu_available(self):
mainmenu.add_item_to_application_menu(
run_action, menu_id=ApplicationMenus.Run)
+ @on_plugin_teardown(plugin=Plugins.Editor)
+ def on_editor_teardown(self):
+ widget = self.get_widget()
+ editor = self.get_plugin(Plugins.Editor)
+ widget.sig_edit_goto_requested.disconnect(editor.load)
+
+ @on_plugin_teardown(plugin=Plugins.Preferences)
+ def on_preferences_teardown(self):
+ preferences = self.get_plugin(Plugins.Preferences)
+ preferences.deregister_plugin_preferences(self)
+
+ @on_plugin_teardown(plugin=Plugins.MainMenu)
+ def on_main_menu_teardown(self):
+ mainmenu = self.get_plugin(Plugins.MainMenu)
+
+ mainmenu.remove_item_from_application_menu(
+ ProfilerActions.ProfileCurrentFile,
+ menu_id=ApplicationMenus.Run
+ )
+
# --- Public API
# ------------------------------------------------------------------------
def run_profiler(self):
diff --git a/spyder/plugins/projects/plugin.py b/spyder/plugins/projects/plugin.py
index 8287ddd70e4..3188ace10f6 100644
--- a/spyder/plugins/projects/plugin.py
+++ b/spyder/plugins/projects/plugin.py
@@ -26,7 +26,8 @@
# Local imports
from spyder.api.exceptions import SpyderAPIError
-from spyder.api.plugin_registration.decorators import on_plugin_available
+from spyder.api.plugin_registration.decorators import (
+ on_plugin_available, on_plugin_teardown)
from spyder.api.translations import get_translation
from spyder.api.plugins import Plugins, SpyderDockablePlugin
from spyder.config.base import (get_home_dir, get_project_config_folder,
@@ -137,7 +138,8 @@ def __init__(self, parent=None, configuration=None):
# ---- SpyderDockablePlugin API
# ------------------------------------------------------------------------
- def get_name(self):
+ @staticmethod
+ def get_name():
return _("Project")
def get_description(self):
@@ -188,18 +190,14 @@ def on_editor_available(self):
treewidget.sig_renamed.connect(self.editor.renamed)
treewidget.sig_tree_renamed.connect(self.editor.renamed_tree)
treewidget.sig_module_created.connect(self.editor.new)
- treewidget.sig_file_created.connect(
- lambda t: self.editor.new(text=t))
+ treewidget.sig_file_created.connect(self._new_editor)
+
+ self.sig_project_loaded.connect(self._setup_editor_files)
+ self.sig_project_closed[bool].connect(self._setup_editor_files)
- self.sig_project_loaded.connect(
- lambda v: self.editor.setup_open_files())
- self.sig_project_closed[bool].connect(
- lambda v: self.editor.setup_open_files())
self.editor.set_projects(self)
- self.sig_project_loaded.connect(
- lambda v: self.editor.set_current_project_path(v))
- self.sig_project_closed.connect(
- lambda v: self.editor.set_current_project_path())
+ self.sig_project_loaded.connect(self._set_path_in_editor)
+ self.sig_project_closed.connect(self._unset_path_in_editor)
@on_plugin_available(plugin=Plugins.Completions)
def on_completions_available(self):
@@ -212,29 +210,17 @@ def on_completions_available(self):
# self.start_workspace_services())
self.completions.sig_stop_completions.connect(
self.stop_workspace_services)
- self.sig_project_loaded.connect(
- functools.partial(self.completions.project_path_update,
- update_kind=WorkspaceUpdateKind.ADDITION,
- instance=self))
- self.sig_project_closed.connect(
- functools.partial(self.completions.project_path_update,
- update_kind=WorkspaceUpdateKind.DELETION,
- instance=self))
+ self.sig_project_loaded.connect(self._add_path_to_completions)
+ self.sig_project_closed.connect(self._remove_path_from_completions)
@on_plugin_available(plugin=Plugins.IPythonConsole)
def on_ipython_console_available(self):
self.ipyconsole = self.get_plugin(Plugins.IPythonConsole)
widget = self.get_widget()
treewidget = widget.treewidget
-
treewidget.sig_open_interpreter_requested.connect(
self.ipyconsole.create_client_from_path)
- treewidget.sig_run_requested.connect(
- lambda fname:
- self.ipyconsole.run_script(
- fname, osp.dirname(fname), '', False, False, False, True,
- False)
- )
+ treewidget.sig_run_requested.connect(self._run_file_in_ipyconsole)
@on_plugin_available(plugin=Plugins.MainMenu)
def on_main_menu_available(self):
@@ -263,6 +249,58 @@ def on_main_menu_available(self):
menu_id=ApplicationMenus.Projects,
section=ProjectsMenuSections.Extras)
+ @on_plugin_teardown(plugin=Plugins.Editor)
+ def on_editor_teardown(self):
+ self.editor = self.get_plugin(Plugins.Editor)
+ widget = self.get_widget()
+ treewidget = widget.treewidget
+
+ treewidget.sig_open_file_requested.disconnect(self.editor.load)
+ treewidget.sig_removed.disconnect(self.editor.removed)
+ treewidget.sig_tree_removed.disconnect(self.editor.removed_tree)
+ treewidget.sig_renamed.disconnect(self.editor.renamed)
+ treewidget.sig_tree_renamed.disconnect(self.editor.renamed_tree)
+ treewidget.sig_module_created.disconnect(self.editor.new)
+ treewidget.sig_file_created.disconnect(self._new_editor)
+
+ self.sig_project_loaded.disconnect(self._setup_editor_files)
+ self.sig_project_closed[bool].disconnect(self._setup_editor_files)
+ self.editor.set_projects(None)
+ self.sig_project_loaded.disconnect(self._set_path_in_editor)
+ self.sig_project_closed.disconnect(self._unset_path_in_editor)
+
+ self.editor = None
+
+ @on_plugin_teardown(plugin=Plugins.Completions)
+ def on_completions_teardown(self):
+ self.completions = self.get_plugin(Plugins.Completions)
+
+ self.completions.sig_stop_completions.disconnect(
+ self.stop_workspace_services)
+
+ self.sig_project_loaded.disconnect(self._add_path_to_completions)
+ self.sig_project_closed.disconnect(self._remove_path_from_completions)
+
+ self.completions = None
+
+ @on_plugin_teardown(plugin=Plugins.IPythonConsole)
+ def on_ipython_console_teardown(self):
+ self.ipyconsole = self.get_plugin(Plugins.IPythonConsole)
+ widget = self.get_widget()
+ treewidget = widget.treewidget
+
+ treewidget.sig_open_interpreter_requested.disconnect(
+ self.ipyconsole.create_client_from_path)
+ treewidget.sig_run_requested.disconnect(self._run_file_in_ipyconsole)
+
+ self._ipython_run_script = None
+ self.ipyconsole = None
+
+ @on_plugin_teardown(plugin=Plugins.MainMenu)
+ def on_main_menu_teardown(self):
+ main_menu = self.get_plugin(Plugins.MainMenu)
+ main_menu.remove_application_menu(ApplicationMenus.Projects)
+
def setup(self):
"""Setup the plugin actions."""
self.create_action(
@@ -904,3 +942,37 @@ def get_project_types(self):
are project type classes.
"""
return self._project_types
+
+ # --- Private API
+ # -------------------------------------------------------------------------
+ def _new_editor(self, text):
+ self.editor.new(text=text)
+
+ def _setup_editor_files(self, __unused):
+ self.editor.setup_open_files()
+
+ def _set_path_in_editor(self, path):
+ self.editor.set_current_project_path(path)
+
+ def _unset_path_in_editor(self, __unused):
+ self.editor.set_current_project_path()
+
+ def _add_path_to_completions(self, path):
+ self.completions.project_path_update(
+ path,
+ update_kind=WorkspaceUpdateKind.ADDITION,
+ instance=self
+ )
+
+ def _remove_path_from_completions(self, path):
+ self.completions.project_path_update(
+ path,
+ update_kind=WorkspaceUpdateKind.DELETION,
+ instance=self
+ )
+
+ def _run_file_in_ipyconsole(self, fname):
+ self.ipyconsole.run_script(
+ fname, osp.dirname(fname), '', False, False, False, True,
+ False
+ )
diff --git a/spyder/plugins/pylint/plugin.py b/spyder/plugins/pylint/plugin.py
index 752f623d285..1adf79faeee 100644
--- a/spyder/plugins/pylint/plugin.py
+++ b/spyder/plugins/pylint/plugin.py
@@ -15,8 +15,10 @@
from qtpy.QtCore import Qt, Signal, Slot
# Local imports
+from spyder.api.exceptions import SpyderAPIError
from spyder.api.plugins import Plugins, SpyderDockablePlugin
-from spyder.api.plugin_registration.decorators import on_plugin_available
+from spyder.api.plugin_registration.decorators import (
+ on_plugin_available, on_plugin_teardown)
from spyder.api.translations import get_translation
from spyder.utils.programs import is_module_installed
from spyder.plugins.mainmenu.api import ApplicationMenus
@@ -60,7 +62,8 @@ class Pylint(SpyderDockablePlugin):
Word to select on given row.
"""
- def get_name(self):
+ @staticmethod
+ def get_name():
return _("Code Analysis")
def get_description(self):
@@ -116,10 +119,9 @@ def on_projects_available(self):
# Connect to projects
projects = self.get_plugin(Plugins.Projects)
- projects.sig_project_loaded.connect(
- lambda value: widget.set_conf("project_dir", value))
- projects.sig_project_closed.connect(
- lambda value: widget.set_conf("project_dir", None))
+
+ projects.sig_project_loaded.connect(self._set_project_dir)
+ projects.sig_project_closed.connect(self._unset_project_dir)
@on_plugin_available(plugin=Plugins.MainMenu)
def on_main_menu_available(self):
@@ -129,6 +131,41 @@ def on_main_menu_available(self):
mainmenu.add_item_to_application_menu(
pylint_act, menu_id=ApplicationMenus.Source)
+ @on_plugin_teardown(plugin=Plugins.Editor)
+ def on_editor_teardown(self):
+ widget = self.get_widget()
+ editor = self.get_plugin(Plugins.Editor)
+
+ # Connect to Editor
+ widget.sig_edit_goto_requested.disconnect(editor.load)
+ editor.sig_editor_focus_changed.disconnect(self._set_filename)
+
+ pylint_act = self.get_action(PylintActions.AnalyzeCurrentFile)
+
+ # TODO: use new API when editor has migrated
+ pylint_act.setVisible(False)
+ editor.pythonfile_dependent_actions.remove(pylint_act)
+
+ @on_plugin_teardown(plugin=Plugins.Preferences)
+ def on_preferences_teardown(self):
+ preferences = self.get_plugin(Plugins.Preferences)
+ preferences.deregister_plugin_preferences(self)
+
+ @on_plugin_teardown(plugin=Plugins.Projects)
+ def on_projects_teardown(self):
+ # Disconnect from projects
+ projects = self.get_plugin(Plugins.Projects)
+ projects.sig_project_loaded.disconnect(self._set_project_dir)
+ projects.sig_project_closed.disconnect(self._unset_project_dir)
+
+ @on_plugin_teardown(plugin=Plugins.MainMenu)
+ def on_main_menu_teardown(self):
+ mainmenu = self.get_plugin(Plugins.MainMenu)
+ mainmenu.remove_item_from_application_menu(
+ PylintActions.AnalyzeCurrentFile,
+ menu_id=ApplicationMenus.Source
+ )
+
# --- Private API
# ------------------------------------------------------------------------
@Slot()
@@ -136,9 +173,21 @@ def _set_filename(self):
"""
Set filename without code analysis.
"""
- editor = self.get_plugin(Plugins.Editor)
- if editor:
- self.get_widget().set_filename(editor.get_current_filename())
+ try:
+ editor = self.get_plugin(Plugins.Editor)
+ if editor:
+ self.get_widget().set_filename(editor.get_current_filename())
+ except SpyderAPIError:
+ # Editor was deleted
+ pass
+
+ def _set_project_dir(self, value):
+ widget = self.get_widget()
+ widget.set_conf("project_dir", value)
+
+ def _unset_project_dir(self, _unused):
+ widget = self.get_widget()
+ widget.set_conf("project_dir", None)
# --- Public API
# ------------------------------------------------------------------------
diff --git a/spyder/plugins/run/plugin.py b/spyder/plugins/run/plugin.py
index 34b1a6c4a61..c8f196991ab 100644
--- a/spyder/plugins/run/plugin.py
+++ b/spyder/plugins/run/plugin.py
@@ -12,7 +12,8 @@
# Local imports
from spyder.api.plugins import Plugins, SpyderPluginV2
-from spyder.api.plugin_registration.decorators import on_plugin_available
+from spyder.api.plugin_registration.decorators import (
+ on_plugin_available, on_plugin_teardown)
from spyder.api.translations import get_translation
from spyder.plugins.run.confpage import RunConfigPage
@@ -37,7 +38,8 @@ class Run(SpyderPluginV2):
# --- SpyderPluginV2 API
# ------------------------------------------------------------------------
- def get_name(self):
+ @staticmethod
+ def get_name():
return _("Run")
def get_description(self):
@@ -54,5 +56,10 @@ def on_preferences_available(self):
preferences = self.get_plugin(Plugins.Preferences)
preferences.register_plugin_preferences(self)
+ @on_plugin_teardown(plugin=Plugins.Preferences)
+ def on_preferences_teardown(self):
+ preferences = self.get_plugin(Plugins.Preferences)
+ preferences.deregister_plugin_preferences(self)
+
# --- Public API
# ------------------------------------------------------------------------
diff --git a/spyder/plugins/shortcuts/plugin.py b/spyder/plugins/shortcuts/plugin.py
index 72291f19873..360ddd25380 100644
--- a/spyder/plugins/shortcuts/plugin.py
+++ b/spyder/plugins/shortcuts/plugin.py
@@ -21,7 +21,8 @@
# Local imports
from spyder.api.plugins import Plugins, SpyderPluginV2
-from spyder.api.plugin_registration.decorators import on_plugin_available
+from spyder.api.plugin_registration.decorators import (
+ on_plugin_available, on_plugin_teardown)
from spyder.api.translations import get_translation
from spyder.plugins.mainmenu.api import ApplicationMenus, HelpMenuSections
from spyder.plugins.shortcuts.confpage import ShortcutsConfigPage
@@ -60,7 +61,8 @@ class Shortcuts(SpyderPluginV2):
# --- SpyderPluginV2 API
# ------------------------------------------------------------------------
- def get_name(self):
+ @staticmethod
+ def get_name():
return _("Keyboard shortcuts")
def get_description(self):
@@ -97,6 +99,19 @@ def on_main_menu_available(self):
section=HelpMenuSections.Documentation,
)
+ @on_plugin_teardown(plugin=Plugins.Preferences)
+ def on_preferences_teardown(self):
+ preferences = self.get_plugin(Plugins.Preferences)
+ preferences.deregister_plugin_preferences(self)
+
+ @on_plugin_teardown(plugin=Plugins.MainMenu)
+ def on_main_menu_teardown(self):
+ mainmenu = self.get_plugin(Plugins.MainMenu)
+ mainmenu.remove_item_from_application_menu(
+ ShortcutActions.ShortcutSummaryAction,
+ menu_id=ApplicationMenus.Help
+ )
+
def on_mainwindow_visible(self):
self.apply_shortcuts()
diff --git a/spyder/plugins/statusbar/plugin.py b/spyder/plugins/statusbar/plugin.py
index 58a5bc0cea7..1f38affb8fc 100644
--- a/spyder/plugins/statusbar/plugin.py
+++ b/spyder/plugins/statusbar/plugin.py
@@ -14,7 +14,8 @@
# Local imports
from spyder.api.exceptions import SpyderAPIError
from spyder.api.plugins import Plugins, SpyderPluginV2
-from spyder.api.plugin_registration.decorators import on_plugin_available
+from spyder.api.plugin_registration.decorators import (
+ on_plugin_available, on_plugin_teardown)
from spyder.api.translations import get_translation
from spyder.api.widgets.status import StatusBarWidget
from spyder.config.base import running_under_pytest
@@ -51,7 +52,8 @@ class StatusBar(SpyderPluginV2):
'vcs_status', 'interpreter_status', 'lsp_status', 'kite_status'}
# ---- SpyderPluginV2 API
- def get_name(self):
+ @staticmethod
+ def get_name():
return _('Status bar')
def get_icon(self):
@@ -67,11 +69,19 @@ def on_initialize(self):
self.add_status_widget(
self.clock_status, StatusBarWidgetPosition.Right)
+ def on_close(self, _unused):
+ self._statusbar.setVisible(False)
+
@on_plugin_available(plugin=Plugins.Preferences)
def on_preferences_available(self):
preferences = self.get_plugin(Plugins.Preferences)
preferences.register_plugin_preferences(self)
+ @on_plugin_teardown(plugin=Plugins.Preferences)
+ def on_preferences_teardown(self):
+ preferences = self.get_plugin(Plugins.Preferences)
+ preferences.deregister_plugin_preferences(self)
+
def after_container_creation(self):
container = self.get_container()
container.sig_show_status_bar_requested.connect(
diff --git a/spyder/plugins/toolbar/container.py b/spyder/plugins/toolbar/container.py
index 338300947a5..d5b735ab4dd 100644
--- a/spyder/plugins/toolbar/container.py
+++ b/spyder/plugins/toolbar/container.py
@@ -212,6 +212,28 @@ def add_application_toolbar(self, toolbar, mainwindow=None):
self._add_missing_toolbar_elements(toolbar, toolbar_id)
+ def remove_application_toolbar(self, toolbar_id: str, mainwindow=None):
+ """
+ Remove toolbar from application toolbars.
+
+ Parameters
+ ----------
+ toolbar: str
+ The application toolbar to remove from the `mainwindow`.
+ mainwindow: QMainWindow
+ The main application window.
+ """
+
+ if toolbar_id not in self._ADDED_TOOLBARS:
+ raise SpyderAPIError(
+ 'Toolbar with ID "{}" is not in the main window'.format(
+ toolbar_id))
+
+ toolbar = self._ADDED_TOOLBARS.pop(toolbar_id)
+ self._toolbarslist.remove(toolbar)
+
+ if mainwindow:
+ mainwindow.removeToolBar(toolbar)
def add_item_to_application_toolbar(self,
item: ToolbarItem,
@@ -250,6 +272,25 @@ def add_item_to_application_toolbar(self,
toolbar.add_item(item, section=section, before=before,
before_section=before_section, omit_id=omit_id)
+ def remove_item_from_application_toolbar(self, item_id: str,
+ toolbar_id: Optional[str] = None):
+ """
+ Remove action or widget from given application toolbar by id.
+
+ Parameters
+ ----------
+ item: str
+ The item to remove from the `toolbar`.
+ toolbar_id: str or None
+ The application toolbar unique string identifier.
+ """
+ if toolbar_id not in self._APPLICATION_TOOLBARS:
+ raise SpyderAPIError(
+ '{} is not a valid toolbar_id'.format(toolbar_id))
+
+ toolbar = self.get_application_toolbar(toolbar_id)
+ toolbar.remove_item(item_id)
+
def get_application_toolbar(self, toolbar_id: str) -> ApplicationToolbar:
"""
Return an application toolbar by toolbar_id.
@@ -304,6 +345,9 @@ def load_last_visible_toolbars(self):
else:
self._get_visible_toolbars()
+ for toolbar in self._visible_toolbars:
+ toolbar.setVisible(True)
+
self.update_actions()
def create_toolbars_menu(self):
diff --git a/spyder/plugins/toolbar/plugin.py b/spyder/plugins/toolbar/plugin.py
index 90d0e72e44e..b7e796ed0e5 100644
--- a/spyder/plugins/toolbar/plugin.py
+++ b/spyder/plugins/toolbar/plugin.py
@@ -15,11 +15,13 @@
# Local imports
from spyder.api.exceptions import SpyderAPIError
from spyder.api.plugins import SpyderPluginV2, Plugins
-from spyder.api.plugin_registration.decorators import on_plugin_available
+from spyder.api.plugin_registration.decorators import (
+ on_plugin_available, on_plugin_teardown)
from spyder.api.translations import get_translation
from spyder.plugins.mainmenu.api import ApplicationMenus, ViewMenuSections
from spyder.plugins.toolbar.api import ApplicationToolbars
-from spyder.plugins.toolbar.container import ToolbarContainer
+from spyder.plugins.toolbar.container import (
+ ToolbarContainer, ToolbarMenus, ToolbarActions)
# Third-party imports
from qtpy.QtWidgets import QWidget
@@ -40,7 +42,8 @@ class Toolbar(SpyderPluginV2):
# --- SpyderDocakblePlugin API
# -----------------------------------------------------------------------
- def get_name(self):
+ @staticmethod
+ def get_name():
return _('Toolbar')
def get_description(self):
@@ -71,6 +74,17 @@ def on_main_menu_available(self):
section=ViewMenuSections.Toolbar,
before_section=ViewMenuSections.Layout)
+ @on_plugin_teardown(plugin=Plugins.MainMenu)
+ def on_main_menu_teardown(self):
+ mainmenu = self.get_plugin(Plugins.MainMenu)
+ # View menu Toolbar section
+ mainmenu.remove_item_from_application_menu(
+ ToolbarMenus.ToolbarsMenu,
+ menu_id=ApplicationMenus.View)
+ mainmenu.remove_item_from_application_menu(
+ ToolbarActions.ShowToolbars,
+ menu_id=ApplicationMenus.View)
+
def on_mainwindow_visible(self):
container = self.get_container()
@@ -103,8 +117,9 @@ def on_mainwindow_visible(self):
def on_close(self, _unused):
container = self.get_container()
- if container._visible_toolbars:
- container._save_visible_toolbars()
+ container._save_visible_toolbars()
+ for toolbar in container._visible_toolbars:
+ toolbar.setVisible(False)
# --- Public API
# ------------------------------------------------------------------------
@@ -143,6 +158,20 @@ def add_application_toolbar(self, toolbar):
"""
self.get_container().add_application_toolbar(toolbar, self._main)
+ def remove_application_toolbar(self, toolbar_id: str):
+ """
+ Remove toolbar from the application toolbars.
+
+ This can be used to remove a custom toolbar. The `WorkingDirectory`
+ plugin is an example of this.
+
+ Parameters
+ ----------
+ toolbar: str
+ The application toolbar to remove from the main window.
+ """
+ self.get_container().remove_application_toolbar(toolbar_id, self._main)
+
def add_item_to_application_toolbar(self,
item: Union[SpyderAction, QWidget],
toolbar_id: Optional[str] = None,
@@ -184,6 +213,23 @@ def add_item_to_application_toolbar(self,
omit_id=omit_id
)
+ def remove_item_from_application_toolbar(self, item_id: str,
+ toolbar_id: Optional[str] = None):
+ """
+ Remove action or widget `item` from given application menu by id.
+
+ Parameters
+ ----------
+ item_id: str
+ The item to remove from the toolbar.
+ toolbar_id: str or None
+ The application toolbar unique string identifier.
+ """
+ self.get_container().remove_item_from_application_toolbar(
+ item_id,
+ toolbar_id=toolbar_id
+ )
+
def get_application_toolbar(self, toolbar_id):
"""
Return an application toolbar by toolbar_id.
diff --git a/spyder/plugins/tours/plugin.py b/spyder/plugins/tours/plugin.py
index 8b041e1ce5c..59fbfe68927 100644
--- a/spyder/plugins/tours/plugin.py
+++ b/spyder/plugins/tours/plugin.py
@@ -12,11 +12,12 @@
# Local imports
from spyder.api.plugins import Plugins, SpyderPluginV2
-from spyder.api.plugin_registration.decorators import on_plugin_available
+from spyder.api.plugin_registration.decorators import (
+ on_plugin_available, on_plugin_teardown)
from spyder.api.translations import get_translation
from spyder.config.base import get_safe_mode, running_under_pytest
from spyder.plugins.application.api import ApplicationActions
-from spyder.plugins.tours.container import ToursContainer
+from spyder.plugins.tours.container import TourActions, ToursContainer
from spyder.plugins.tours.tours import INTRO_TOUR, TourIdentifiers
from spyder.plugins.mainmenu.api import ApplicationMenus, HelpMenuSections
@@ -38,7 +39,8 @@ class Tours(SpyderPluginV2):
# --- SpyderPluginV2 API
# ------------------------------------------------------------------------
- def get_name(self):
+ @staticmethod
+ def get_name():
return _("Interactive tours")
def get_description(self):
@@ -64,6 +66,13 @@ def on_main_menu_available(self):
section=HelpMenuSections.Documentation,
before=ApplicationActions.SpyderDocumentationAction)
+ @on_plugin_teardown(plugin=Plugins.MainMenu)
+ def on_main_menu_teardown(self):
+ mainmenu = self.get_plugin(Plugins.MainMenu)
+ mainmenu.remove_item_from_application_menu(
+ TourActions.ShowTour,
+ menu_id=ApplicationMenus.Help)
+
def on_mainwindow_visible(self):
self.show_tour_message()
diff --git a/spyder/plugins/variableexplorer/plugin.py b/spyder/plugins/variableexplorer/plugin.py
index 279cc6c88e5..ddc79c882eb 100644
--- a/spyder/plugins/variableexplorer/plugin.py
+++ b/spyder/plugins/variableexplorer/plugin.py
@@ -10,7 +10,8 @@
# Local imports
from spyder.api.plugins import Plugins, SpyderDockablePlugin
-from spyder.api.plugin_registration.decorators import on_plugin_available
+from spyder.api.plugin_registration.decorators import (
+ on_plugin_available, on_plugin_teardown)
from spyder.api.translations import get_translation
from spyder.plugins.variableexplorer.confpage import (
VariableExplorerConfigPage)
@@ -37,7 +38,8 @@ class VariableExplorer(SpyderDockablePlugin):
# ---- SpyderDockablePlugin API
# ------------------------------------------------------------------------
- def get_name(self):
+ @staticmethod
+ def get_name():
return _('Variable explorer')
def get_description(self):
@@ -67,8 +69,13 @@ def on_ipyconsole_available(self):
ipyconsole.sig_shellwidget_deleted.connect(
self.remove_shellwidget)
- def unregister(self):
- # Plugins
+ @on_plugin_teardown(plugin=Plugins.Preferences)
+ def on_preferences_teardown(self):
+ preferences = self.get_plugin(Plugins.Preferences)
+ preferences.deregister_plugin_preferences(self)
+
+ @on_plugin_teardown(plugin=Plugins.IPythonConsole)
+ def on_ipyconsole_teardown(self):
ipyconsole = self.get_plugin(Plugins.IPythonConsole)
# Signals
diff --git a/spyder/plugins/workingdirectory/plugin.py b/spyder/plugins/workingdirectory/plugin.py
index b46e7d36e82..0bd75855856 100644
--- a/spyder/plugins/workingdirectory/plugin.py
+++ b/spyder/plugins/workingdirectory/plugin.py
@@ -16,12 +16,14 @@
# Local imports
from spyder.api.plugins import SpyderPluginV2, Plugins
-from spyder.api.plugin_registration.decorators import on_plugin_available
+from spyder.api.plugin_registration.decorators import (
+ on_plugin_available, on_plugin_teardown)
from spyder.api.translations import get_translation
from spyder.config.base import get_conf_path
from spyder.plugins.workingdirectory.confpage import WorkingDirectoryConfigPage
from spyder.plugins.workingdirectory.container import (
WorkingDirectoryContainer)
+from spyder.plugins.toolbar.api import ApplicationToolbars
from spyder.utils import encoding
# Localization
@@ -57,7 +59,8 @@ class WorkingDirectory(SpyderPluginV2):
# --- SpyderPluginV2 API
# ------------------------------------------------------------------------
- def get_name(self):
+ @staticmethod
+ def get_name():
return _('Current working directory')
def get_description(self):
@@ -90,17 +93,13 @@ def on_preferences_available(self):
@on_plugin_available(plugin=Plugins.Editor)
def on_editor_available(self):
editor = self.get_plugin(Plugins.Editor)
- editor.sig_dir_opened.connect(
- lambda path, plugin=editor: self.chdir(path, editor))
+ editor.sig_dir_opened.connect(self._editor_change_dir)
@on_plugin_available(plugin=Plugins.Explorer)
def on_explorer_available(self):
explorer = self.get_plugin(Plugins.Explorer)
-
- self.sig_current_directory_changed.connect(
- lambda path: explorer.chdir(path, emit=False))
- explorer.sig_dir_opened.connect(
- lambda path, plugin=explorer: self.chdir(path, plugin))
+ self.sig_current_directory_changed.connect(self._explorer_change_dir)
+ explorer.sig_dir_opened.connect(self._explorer_dir_opened)
@on_plugin_available(plugin=Plugins.IPythonConsole)
def on_ipyconsole_available(self):
@@ -108,28 +107,51 @@ def on_ipyconsole_available(self):
self.sig_current_directory_changed.connect(
ipyconsole.set_current_client_working_directory)
- # TODO: chdir_current_client might follow a better naming
- # convention
ipyconsole.sig_current_directory_changed.connect(
- lambda path, plugin=ipyconsole: self.chdir(path, plugin))
+ self._ipyconsole_change_dir)
@on_plugin_available(plugin=Plugins.Projects)
def on_projects_available(self):
projects = self.get_plugin(Plugins.Projects)
- projects.sig_project_loaded.connect(
- lambda path:
- self.chdir(
- directory=path,
- sender_plugin=projects
- )
- )
+ projects.sig_project_loaded.connect(self._project_loaded)
+ projects.sig_project_closed[object].connect(self._project_closed)
- projects.sig_project_closed[object].connect(
- lambda path: self.chdir(
- directory=projects.get_last_working_dir(),
- sender_plugin=projects
- )
- )
+ @on_plugin_teardown(plugin=Plugins.Toolbar)
+ def on_toolbar_teardown(self):
+ toolbar = self.get_plugin(Plugins.Toolbar)
+ toolbar.remove_application_toolbar(
+ ApplicationToolbars.WorkingDirectory)
+
+ @on_plugin_teardown(plugin=Plugins.Preferences)
+ def on_preferences_teardown(self):
+ preferences = self.get_plugin(Plugins.Preferences)
+ preferences.deregister_plugin_preferences(self)
+
+ @on_plugin_teardown(plugin=Plugins.Editor)
+ def on_editor_teardown(self):
+ editor = self.get_plugin(Plugins.Editor)
+ editor.sig_dir_opened.disconnect(self._editor_change_dir)
+
+ @on_plugin_teardown(plugin=Plugins.Explorer)
+ def on_explorer_teardown(self):
+ explorer = self.get_plugin(Plugins.Explorer)
+ self.sig_current_directory_changed.disconnect(self._explorer_change_dir)
+ explorer.sig_dir_opened.disconnect(self._explorer_dir_opened)
+
+ @on_plugin_teardown(plugin=Plugins.IPythonConsole)
+ def on_ipyconsole_teardown(self):
+ ipyconsole = self.get_plugin(Plugins.IPythonConsole)
+
+ self.sig_current_directory_changed.disconnect(
+ ipyconsole.set_current_client_working_directory)
+ ipyconsole.sig_current_directory_changed.disconnect(
+ self._ipyconsole_change_dir)
+
+ @on_plugin_teardown(plugin=Plugins.Projects)
+ def on_projects_teardown(self):
+ projects = self.get_plugin(Plugins.Projects)
+ projects.sig_project_loaded.disconnect(self._project_loaded)
+ projects.sig_project_closed[object].disconnect(self._project_closed)
# --- Public API
# ------------------------------------------------------------------------
@@ -205,3 +227,31 @@ def get_workdir(self):
Current working directory.
"""
return self.get_container().get_workdir()
+
+ # -------------------------- Private API ----------------------------------
+ def _editor_change_dir(self, path):
+ editor = self.get_plugin(Plugins.Editor)
+ self.chdir(path, editor)
+
+ def _explorer_change_dir(self, path):
+ explorer = self.get_plugin(Plugins.Explorer)
+ explorer.chdir(path, emit=False)
+
+ def _explorer_dir_opened(self, path):
+ explorer = self.get_plugin(Plugins.Explorer)
+ self.chdir(path, explorer)
+
+ def _ipyconsole_change_dir(self, path):
+ ipyconsole = self.get_plugin(Plugins.IPythonConsole)
+ self.chdir(path, ipyconsole)
+
+ def _project_loaded(self, path):
+ projects = self.get_plugin(Plugins.Projects)
+ self.chdir(directory=path, sender_plugin=projects)
+
+ def _project_closed(self, path):
+ projects = self.get_plugin(Plugins.Projects)
+ self.chdir(
+ directory=projects.get_last_working_dir(),
+ sender_plugin=projects
+ )
diff --git a/spyder/utils/icon_manager.py b/spyder/utils/icon_manager.py
index 11c404c7014..8bc7a153594 100644
--- a/spyder/utils/icon_manager.py
+++ b/spyder/utils/icon_manager.py
@@ -334,6 +334,8 @@ def __init__(self):
# --- Status bar --------------------------------------------------------
'code_fork': [('mdi.source-fork',), {'color': self.MAIN_FG_COLOR}],
'statusbar': [('mdi.dock-bottom',), {'color': self.MAIN_FG_COLOR}],
+ # --- Plugin registry ---------------------------------------------------
+ 'plugins': [('mdi.puzzle',), {'color': self.MAIN_FG_COLOR}],
}
def get_std_icon(self, name, size=None):