Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[spy-plugin-registry-teardown] PR: Migrate Console, Explorer and Find in files to use the new teardown mechanism #4

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
d6919ae
Migrate Console, Explorer and Find in files to use the new teardown m…
andfoy Oct 5, 2021
6adb666
Minor typo correction
andfoy Oct 5, 2021
bce5545
Prevent editor from overriding actions in Search menu
andfoy Oct 5, 2021
bd1b27d
Prevent non-existinng search_menu_actions list
andfoy Oct 5, 2021
505954b
Update calls to remove_item_from_application_menu
andfoy Oct 5, 2021
e4c9c4f
Migrate help to use the new teardown mechanism
andfoy Oct 5, 2021
89f31a1
Update call to remove_item_from_application_menu
andfoy Oct 5, 2021
8af7fbd
Apply review comments
andfoy Oct 5, 2021
80157cd
Migrate history and layouts to use the new teardown mmechanism
andfoy Oct 5, 2021
1ec50d4
Update calls to main menu and toolbar to use ids
andfoy Oct 5, 2021
f58c75f
Address review comments
andfoy Oct 5, 2021
9b6869a
Migrate Outline explorer, plots, preferences and profiler to use the …
andfoy Oct 5, 2021
554301c
Update profiler calls to remove_item_from_application_menu
andfoy Oct 5, 2021
60808bd
Address review comments
andfoy Oct 5, 2021
fe88c88
Migrate projects to use the new teardown mechanism
andfoy Oct 5, 2021
6bd0b9d
Update calls to remove_item_from_application_menu
andfoy Oct 5, 2021
72e60d1
Address review comments
andfoy Oct 5, 2021
86045af
Address further review comments
andfoy Oct 5, 2021
7370c40
Minor error correction
andfoy Oct 5, 2021
a22feb1
Restore sig_stop_completions connection
andfoy Oct 5, 2021
158338a
Migrate pylint, run and shortcuts to use the new teardown mechanism
andfoy Oct 5, 2021
66e1ebe
Remove hard reference to editor in pylint
andfoy Oct 5, 2021
17167be
Update calls to remove_item_from_application_menu
andfoy Oct 5, 2021
2e4ddff
Address review comments
andfoy Oct 5, 2021
eed84fd
Final review comments
andfoy Oct 5, 2021
aa426e5
Migrate statusbar, toolbar and tours to use the new teardown mechanism
andfoy Oct 5, 2021
c29806d
Update calls to remove_item_from_application_menu
andfoy Oct 5, 2021
4fd7920
Address review comments
andfoy Oct 5, 2021
aad80bd
Add proper signature to statusbar on_close method
andfoy Oct 5, 2021
b5f5e5e
Address review comments
andfoy Oct 5, 2021
d07cdf4
Migrate variable explorer and working directory to use the new teardo…
andfoy Oct 5, 2021
86fd890
Update calls to remove_item_from_application_menu
andfoy Oct 5, 2021
ee60ca0
Apply review comments
andfoy Oct 5, 2021
d31892c
Remove duplicate on__close
andfoy Oct 5, 2021
7641139
Add preference page to enable and disable plugins
andfoy Oct 5, 2021
b36c492
Restart Spyder after a plugin is enabled/disabled
andfoy Oct 5, 2021
7b8f52f
Disable also Spyder 4 plugins
andfoy Oct 5, 2021
522d158
Fix test_preferences_checkboxes_not_checked_regression
andfoy Oct 5, 2021
2d8c268
Address review comments
andfoy Oct 5, 2021
9f84e98
Always add external plugin metadata
andfoy Oct 6, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions spyder/api/plugin_registration/_confpage.py
Original file line number Diff line number Diff line change
@@ -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({})
82 changes: 74 additions & 8 deletions spyder/api/plugin_registration/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand Down Expand Up @@ -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]]]
Expand All @@ -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`."""
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.

Expand All @@ -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
-------
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion spyder/api/plugins/new_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
14 changes: 12 additions & 2 deletions spyder/api/preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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]
Expand Down
12 changes: 12 additions & 0 deletions spyder/api/widgets/toolbars.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading