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 help to use the new teardown mechanism #5

Merged
merged 35 commits into from
Oct 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
0b26628
Migrate help to use the new teardown mechanism
andfoy Oct 5, 2021
4cac49a
Update call to remove_item_from_application_menu
andfoy Oct 5, 2021
38dd9d4
Apply review comments
andfoy Oct 5, 2021
d3fafcb
Migrate history and layouts to use the new teardown mmechanism
andfoy Oct 5, 2021
17f6510
Update calls to main menu and toolbar to use ids
andfoy Oct 5, 2021
76f7312
Address review comments
andfoy Oct 5, 2021
45d63f7
Migrate Outline explorer, plots, preferences and profiler to use the …
andfoy Oct 5, 2021
d83e901
Update profiler calls to remove_item_from_application_menu
andfoy Oct 5, 2021
a03c088
Address review comments
andfoy Oct 5, 2021
5883605
Migrate projects to use the new teardown mechanism
andfoy Oct 5, 2021
b558f62
Update calls to remove_item_from_application_menu
andfoy Oct 5, 2021
3d1d7ac
Address review comments
andfoy Oct 5, 2021
2a78920
Address further review comments
andfoy Oct 5, 2021
c4a1bde
Minor error correction
andfoy Oct 5, 2021
97dc42f
Restore sig_stop_completions connection
andfoy Oct 5, 2021
30985ba
Migrate pylint, run and shortcuts to use the new teardown mechanism
andfoy Oct 5, 2021
ffde5ad
Remove hard reference to editor in pylint
andfoy Oct 5, 2021
65009a4
Update calls to remove_item_from_application_menu
andfoy Oct 5, 2021
2acdd1f
Address review comments
andfoy Oct 5, 2021
2bd4809
Final review comments
andfoy Oct 5, 2021
e57cc38
Migrate statusbar, toolbar and tours to use the new teardown mechanism
andfoy Oct 5, 2021
fe38a97
Update calls to remove_item_from_application_menu
andfoy Oct 5, 2021
068550e
Address review comments
andfoy Oct 5, 2021
a7c273c
Add proper signature to statusbar on_close method
andfoy Oct 5, 2021
7362068
Address review comments
andfoy Oct 5, 2021
60af997
Migrate variable explorer and working directory to use the new teardo…
andfoy Oct 5, 2021
6e0f6e9
Update calls to remove_item_from_application_menu
andfoy Oct 5, 2021
fd705bd
Apply review comments
andfoy Oct 5, 2021
a02af89
Remove duplicate on__close
andfoy Oct 5, 2021
f542764
Add preference page to enable and disable plugins
andfoy Oct 5, 2021
1230dbc
Restart Spyder after a plugin is enabled/disabled
andfoy Oct 5, 2021
79c16f0
Disable also Spyder 4 plugins
andfoy Oct 5, 2021
6a62371
Fix test_preferences_checkboxes_not_checked_regression
andfoy Oct 5, 2021
aa1af69
Address review comments
andfoy Oct 5, 2021
c06bfc7
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