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 breakpoints to use the new teardown mechanism #2

Merged
merged 48 commits into from
Oct 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
3d27687
Migrate breakpoints to use the new teardown mechanism
andfoy Oct 5, 2021
5448f61
Update remove_item_from_application_menu call
andfoy Oct 5, 2021
9ee88ac
Migrate completions plugin to use the teardown mechanism
andfoy Oct 5, 2021
a644ac0
Remove kite-defined actions
andfoy Oct 5, 2021
a7a6ba8
Fix minor issue in Kite
andfoy Oct 5, 2021
6eaf52e
Reimplement can_close
andfoy Oct 5, 2021
c064b46
Update remove_item_from_application_menu calls
andfoy Oct 5, 2021
8d78ecb
Add remove_application_menu to main menu
andfoy Oct 5, 2021
18e9977
Remove unregister method
andfoy Oct 5, 2021
26e8eb9
Migrate Console, Explorer and Find in files to use the new teardown m…
andfoy Oct 5, 2021
aef911c
Minor typo correction
andfoy Oct 5, 2021
b39e416
Prevent non-existinng search_menu_actions list
andfoy Oct 5, 2021
a702949
Update calls to remove_item_from_application_menu
andfoy Oct 5, 2021
b55159e
Migrate help to use the new teardown mechanism
andfoy Oct 5, 2021
d2d5ee1
Update call to remove_item_from_application_menu
andfoy Oct 5, 2021
44872da
Apply review comments
andfoy Oct 5, 2021
c79bbf6
Migrate history and layouts to use the new teardown mmechanism
andfoy Oct 5, 2021
c32f238
Update calls to main menu and toolbar to use ids
andfoy Oct 5, 2021
f87ec37
Address review comments
andfoy Oct 5, 2021
60e6810
Migrate Outline explorer, plots, preferences and profiler to use the …
andfoy Oct 5, 2021
be3014d
Update profiler calls to remove_item_from_application_menu
andfoy Oct 5, 2021
b0a0564
Address review comments
andfoy Oct 5, 2021
4374df0
Migrate projects to use the new teardown mechanism
andfoy Oct 5, 2021
5fb2c09
Update calls to remove_item_from_application_menu
andfoy Oct 5, 2021
d11869c
Address review comments
andfoy Oct 5, 2021
94c71be
Address further review comments
andfoy Oct 5, 2021
7cc999e
Minor error correction
andfoy Oct 5, 2021
e3464a1
Restore sig_stop_completions connection
andfoy Oct 5, 2021
9f5530f
Migrate pylint, run and shortcuts to use the new teardown mechanism
andfoy Oct 5, 2021
67a9290
Remove hard reference to editor in pylint
andfoy Oct 5, 2021
3dbe853
Update calls to remove_item_from_application_menu
andfoy Oct 5, 2021
ff81fe3
Address review comments
andfoy Oct 5, 2021
5b2ef7a
Final review comments
andfoy Oct 5, 2021
7bae6a5
Migrate statusbar, toolbar and tours to use the new teardown mechanism
andfoy Oct 5, 2021
f894e52
Update calls to remove_item_from_application_menu
andfoy Oct 5, 2021
258e4cf
Address review comments
andfoy Oct 5, 2021
fd5e080
Add proper signature to statusbar on_close method
andfoy Oct 5, 2021
dfd6dc8
Address review comments
andfoy Oct 5, 2021
46c110b
Migrate variable explorer and working directory to use the new teardo…
andfoy Oct 5, 2021
f3e2225
Update calls to remove_item_from_application_menu
andfoy Oct 5, 2021
ff95626
Apply review comments
andfoy Oct 5, 2021
f82445e
Remove duplicate on__close
andfoy Oct 5, 2021
cf5beef
Add preference page to enable and disable plugins
andfoy Oct 5, 2021
5e57da5
Restart Spyder after a plugin is enabled/disabled
andfoy Oct 5, 2021
3a36062
Disable also Spyder 4 plugins
andfoy Oct 5, 2021
c1ae0d7
Fix test_preferences_checkboxes_not_checked_regression
andfoy Oct 5, 2021
b6e8b9f
Address review comments
andfoy Oct 5, 2021
2b643c8
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