# -*- coding: utf-8 -*-
#
# Copyright © Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see spyder/__init__.py for details)
"""Global plugin registry."""
# Standard library imports
import logging
from typing import Dict, List, Union, Type, Any, Set, Optional
# Third-party library imports
from qtpy.QtCore import QObject, Signal
# Local imports
from spyder import dependencies
from spyder.api.translations import _
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.exceptions import SpyderAPIError
from spyder.api.plugins import Plugins, SpyderDockablePlugin, SpyderPluginV2
from spyder.utils.icon_manager import ima
SpyderPluginClass = Union[SpyderPluginV2, SpyderDockablePlugin]
ALL_PLUGINS = [getattr(Plugins, attr) for attr in dir(Plugins)
if not attr.startswith('_') and attr != 'All']
logger = logging.getLogger(__name__)
[docs]
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
[docs]
class SpyderPluginRegistry(QObject, PreferencesAdapter):
"""
Global plugin registry.
This class handles a plugin initialization/teardown lifetime, including
notifications when a plugin is available or not.
This registry alleviates the limitations of a topological sort-based
plugin initialization by enabling plugins to have bidirectional
dependencies instead of unidirectional ones.
Notes
-----
1. This class should be instantiated as a singleton.
2. A plugin should not depend on other plugin to perform its
initialization since it could cause deadlocks.
"""
sig_plugin_ready = Signal(str, bool)
"""
This signal is used to let the main window know that a plugin is ready.
Parameters
----------
plugin_name: str
Name of the plugin that is available.
omit_conf: bool
True if the plugin configuration does not need to be written.
"""
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]]]
# Dictionary that maps a plugin name to a list of the plugin names
# that the plugin depends on.
self.plugin_dependencies = {} # type: Dict[str, Dict[str, List[str]]]
# Plugin dictionary mapped by their names
self.plugin_registry = {} # type: Dict[str, SpyderPluginClass]
# Dictionary that maps a plugin name to its availability.
self.plugin_availability = {} # type: Dict[str, bool]
# Set that stores the plugin names of all Spyder 4 plugins.
self.old_plugins = set({}) # type: set[str]
# Set that stores the names of the plugins that are enabled
self.enabled_plugins = set({}) # type: set[str]
# Set that stores the names of the internal plugins
self.internal_plugins = set({}) # type: set[str]
# 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]]]
# This is used to allow disabling external plugins through Preferences
self._external_plugins_conf_section = "external_plugins"
# ------------------------- PRIVATE API -----------------------------------
def _update_dependents(self, plugin: str, dependent_plugin: str, key: str):
"""Add `dependent_plugin` to the list of dependents of `plugin`."""
plugin_dependents = self.plugin_dependents.get(plugin, {})
plugin_strict_dependents = plugin_dependents.get(key, [])
plugin_strict_dependents.append(dependent_plugin)
plugin_dependents[key] = plugin_strict_dependents
self.plugin_dependents[plugin] = plugin_dependents
def _update_dependencies(self, plugin: str, required_plugin: str,
key: str):
"""Add `required_plugin` to the list of dependencies of `plugin`."""
plugin_dependencies = self.plugin_dependencies.get(plugin, {})
plugin_strict_dependencies = plugin_dependencies.get(key, [])
plugin_strict_dependencies.append(required_plugin)
plugin_dependencies[key] = plugin_strict_dependencies
self.plugin_dependencies[plugin] = plugin_dependencies
def _update_plugin_info(self, plugin_name: str,
required_plugins: List[str],
optional_plugins: List[str]):
"""Update the dependencies and dependents of `plugin_name`."""
for plugin in required_plugins:
self._update_dependencies(plugin_name, plugin, 'requires')
self._update_dependents(plugin, plugin_name, 'requires')
for plugin in optional_plugins:
self._update_dependencies(plugin_name, plugin, 'optional')
self._update_dependents(plugin, plugin_name, 'optional')
def _instantiate_spyder_plugin(
self,
main_window: Any,
PluginClass: Type[SpyderPluginClass],
external: bool,
) -> SpyderPluginClass:
"""Instantiate and register a Spyder 5+ plugin."""
required_plugins = list(set(PluginClass.REQUIRES))
optional_plugins = list(set(PluginClass.OPTIONAL))
plugin_name = PluginClass.NAME
logger.debug(f'Registering plugin {plugin_name} - {PluginClass}')
if PluginClass.CONF_FILE:
CONF.register_plugin(PluginClass)
for plugin in list(required_plugins):
if plugin == Plugins.All:
required_plugins = list(set(required_plugins + ALL_PLUGINS))
for plugin in list(optional_plugins):
if plugin == Plugins.All:
optional_plugins = list(set(optional_plugins + ALL_PLUGINS))
# Update plugin dependency information
self._update_plugin_info(plugin_name, required_plugins,
optional_plugins)
# Create and store plugin instance
plugin_instance = PluginClass(main_window, configuration=CONF)
self.plugin_registry[plugin_name] = plugin_instance
# Connect plugin availability signal to notification system
plugin_instance.sig_plugin_ready.connect(
lambda: self.notify_plugin_availability(
plugin_name, omit_conf=PluginClass.CONF_FILE))
# Initialize plugin instance
plugin_instance.initialize()
# Register plugins that are already available
self._notify_plugin_dependencies(plugin_name)
# Register the plugin name under the external or internal
# plugin set
if external:
self.external_plugins |= {plugin_name}
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 _notify_plugin_dependencies(self, plugin_name: str):
"""Notify a plugin of its available dependencies."""
plugin_instance = self.plugin_registry[plugin_name]
plugin_dependencies = self.plugin_dependencies.get(plugin_name, {})
required_plugins = plugin_dependencies.get('requires', [])
optional_plugins = plugin_dependencies.get('optional', [])
for plugin in required_plugins + optional_plugins:
if plugin in self.plugin_registry:
if self.plugin_availability.get(plugin, False):
logger.debug(f'Plugin {plugin} has already loaded')
plugin_instance._on_plugin_available(plugin)
def _notify_plugin_teardown(self, plugin_name: str):
"""Notify dependents of a plugin that is going to be unavailable."""
plugin_dependents = self.plugin_dependents.get(plugin_name, {})
required_plugins = plugin_dependents.get('requires', [])
optional_plugins = plugin_dependents.get('optional', [])
for plugin in required_plugins + optional_plugins:
if plugin in self.plugin_registry:
if self.plugin_availability.get(plugin, False):
logger.debug(f'Notifying plugin {plugin} that '
f'{plugin_name} is going to be turned off')
plugin_instance = self.plugin_registry[plugin]
plugin_instance._on_plugin_teardown(plugin_name)
def _teardown_plugin(self, plugin_name: str):
"""Disconnect a plugin from its dependencies."""
plugin_instance = self.plugin_registry[plugin_name]
plugin_dependencies = self.plugin_dependencies.get(plugin_name, {})
required_plugins = plugin_dependencies.get('requires', [])
optional_plugins = plugin_dependencies.get('optional', [])
for plugin in required_plugins + optional_plugins:
if plugin in self.plugin_registry:
if self.plugin_availability.get(plugin, False):
logger.debug(f'Disconnecting {plugin_name} from {plugin}')
plugin_instance._on_plugin_teardown(plugin)
# -------------------------- PUBLIC API -----------------------------------
[docs]
def register_plugin(
self, main_window: Any,
PluginClass: Type[SpyderPluginClass],
*args: tuple, external: bool = False,
**kwargs: dict) -> SpyderPluginClass:
"""
Register a plugin into the Spyder registry.
Parameters
----------
main_window: spyder.app.mainwindow.MainWindow
Reference to Spyder's main window.
PluginClass: type[SpyderPluginClass]
The plugin class to register and create. It must be one of
`spyder.app.registry.SpyderPluginClass`.
*args: tuple
Positional arguments used to initialize the plugin
instance.
external: bool
If True, then the plugin is stored as a external plugin. Otherwise
it will be marked as an internal plugin. Default: False
**kwargs: dict
Optional keyword arguments used to initialize the plugin instance.
Returns
-------
plugin: SpyderPluginClass
The instance of the registered plugin.
Raises
------
TypeError
If the `PluginClass` does not inherit from any of
`spyder.app.registry.SpyderPluginClass`
Notes
-----
The optional `*args` and `**kwargs` will be removed once all
plugins are migrated.
"""
if not issubclass(PluginClass, SpyderPluginV2):
raise TypeError(
f"{PluginClass} does not inherit from SpyderPluginV2"
)
instance = None
if issubclass(PluginClass, SpyderPluginV2):
# Register a Spyder 5+ plugin
instance = self._instantiate_spyder_plugin(
main_window, PluginClass, external
)
return instance
[docs]
def notify_plugin_availability(self, plugin_name: str,
notify_main: bool = True,
omit_conf: bool = False):
"""
Notify dependent plugins of a given plugin of its availability.
Parameters
----------
plugin_name: str
Name of the plugin that is available.
notify_main: bool
If True, then a signal is emitted to the main window to perform
further registration steps.
omit_conf: bool
If True, then the main window is instructed to not write the
plugin configuration into the Spyder configuration file.
"""
logger.debug(f'Plugin {plugin_name} has finished loading, '
'sending notifications')
# Set plugin availability to True
self.plugin_availability[plugin_name] = True
# Notify the main window that the plugin is ready
if notify_main:
self.sig_plugin_ready.emit(plugin_name, omit_conf)
# Notify plugin dependents
plugin_dependents = self.plugin_dependents.get(plugin_name, {})
required_plugins = plugin_dependents.get('requires', [])
optional_plugins = plugin_dependents.get('optional', [])
for plugin in required_plugins + optional_plugins:
if plugin in self.plugin_registry:
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)
[docs]
def can_delete_plugin(self, plugin_name: str) -> bool:
"""
Check if a plugin from the registry can be deleted by its name.
Paremeters
----------
plugin_name: str
Name of the plugin to check for deletion.
Returns
-------
can_close: bool
True if the plugin can be closed. False otherwise.
"""
plugin_instance = self.plugin_registry[plugin_name]
# Determine if plugin can be closed
return plugin_instance.can_close()
[docs]
def dock_undocked_plugin(
self, plugin_name: str, save_undocked: bool = False):
"""
Dock plugin if undocked and save undocked state if requested
Parameters
----------
plugin_name: str
Name of the plugin to check for deletion.
save_undocked : bool, optional
True if the undocked state needs to be saved. The default is False.
Returns
-------
None.
"""
plugin_instance = self.plugin_registry[plugin_name]
if isinstance(plugin_instance, SpyderDockablePlugin):
# Close undocked plugin if needed and save undocked state
plugin_instance.close_window(save_undocked=save_undocked)
[docs]
def delete_plugin(self, plugin_name: str, teardown: bool = True,
check_can_delete: bool = True) -> bool:
"""
Remove and delete a plugin from the registry by its name.
Paremeters
----------
plugin_name: str
Name of the plugin to delete.
teardown: bool
True if the teardown notification to other plugins should be sent
when deleting the plugin, False otherwise.
check_can_delete: bool
True if the plugin should validate if it can be closed when this
method is called, False otherwise.
Returns
-------
plugin_deleted: bool
True if the registry was able to teardown and remove the plugin.
False otherwise.
"""
logger.debug(f'Deleting plugin {plugin_name}')
plugin_instance = self.plugin_registry[plugin_name]
# Determine if plugin can be closed
if check_can_delete:
can_delete = self.can_delete_plugin(plugin_name)
if not can_delete:
return False
if isinstance(plugin_instance, SpyderPluginV2):
# Cleanly delete plugin widgets. This avoids segfautls with
# PyQt 5.15
if isinstance(plugin_instance, SpyderDockablePlugin):
try:
plugin_instance.get_widget().close()
plugin_instance.get_widget().deleteLater()
except RuntimeError:
pass
else:
container = plugin_instance.get_container()
if container:
try:
container.close()
container.deleteLater()
except RuntimeError:
pass
# Delete plugin
try:
plugin_instance.deleteLater()
except RuntimeError:
pass
if teardown:
# Disconnect plugin from other plugins
self._teardown_plugin(plugin_name)
# Disconnect depending plugins from the plugin to delete
self._notify_plugin_teardown(plugin_name)
# Perform plugin closure tasks
try:
plugin_instance.on_close(True)
except RuntimeError:
pass
# Delete plugin from the registry and auxiliary structures
self.plugin_dependents.pop(plugin_name, None)
self.plugin_dependencies.pop(plugin_name, None)
if plugin_instance.CONF_FILE:
# This must be done after on_close() so that plugins can modify
# their (external) config therein.
CONF.unregister_plugin(plugin_instance)
for plugin in self.plugin_dependents:
all_plugin_dependents = self.plugin_dependents[plugin]
for key in {'requires', 'optional'}:
plugin_dependents = all_plugin_dependents.get(key, [])
if plugin_name in plugin_dependents:
plugin_dependents.remove(plugin_name)
for plugin in self.plugin_dependencies:
all_plugin_dependencies = self.plugin_dependencies[plugin]
for key in {'requires', 'optional'}:
plugin_dependencies = all_plugin_dependencies.get(key, [])
if plugin_name in plugin_dependencies:
plugin_dependencies.remove(plugin_name)
self.plugin_availability.pop(plugin_name)
self.old_plugins -= {plugin_name}
self.enabled_plugins -= {plugin_name}
self.internal_plugins -= {plugin_name}
self.external_plugins -= {plugin_name}
# Remove the plugin from the registry
self.plugin_registry.pop(plugin_name)
return True
[docs]
def dock_all_undocked_plugins(self, save_undocked: bool = False):
"""
Dock undocked plugins and save undocked state if required.
Parameters
----------
save_undocked : bool, optional
True if the undocked state needs to be saved. The default is False.
Returns
-------
None.
"""
for plugin_name in (
set(self.external_plugins) | set(self.internal_plugins)):
self.dock_undocked_plugin(
plugin_name, save_undocked=save_undocked)
[docs]
def can_delete_all_plugins(self,
excluding: Optional[Set[str]] = None) -> bool:
"""
Determine if all plugins can be deleted except the ones to exclude.
Parameters
----------
excluding: Optional[Set[str]]
A set that lists plugins (by name) that will not be deleted.
Returns
-------
bool
True if all plugins can be closed. False otherwise.
"""
excluding = excluding or set({})
can_close = True
# Check external plugins
for plugin_name in (
set(self.external_plugins) | set(self.internal_plugins)):
if plugin_name not in excluding:
can_close &= self.can_delete_plugin(plugin_name)
if not can_close:
break
return can_close
[docs]
def delete_all_plugins(self, excluding: Optional[Set[str]] = None,
close_immediately: bool = False) -> bool:
"""
Remove all plugins from the registry.
The teardown mechanism will remove external plugins first and then
internal ones, where the Spyder 4 plugins will be removed first and
then the Spyder 5 ones.
Parameters
----------
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
-------
all_deleted: bool
True if all the plugins were closed and deleted. False otherwise.
"""
excluding = excluding or set({})
can_close = True
# Check if all the plugins can be closed
can_close = self.can_delete_all_plugins(excluding=excluding)
if not can_close and not close_immediately:
return False
# Delete Spyder 5+ external plugins
for plugin_name in set(self.external_plugins):
if plugin_name not in excluding:
plugin_instance = self.plugin_registry[plugin_name]
if isinstance(plugin_instance, SpyderPluginV2):
can_close &= self.delete_plugin(
plugin_name, teardown=False, check_can_delete=False)
if not can_close and not close_immediately:
break
if not can_close and not close_immediately:
return False
# Delete Spyder 5+ internal plugins
for plugin_name in set(self.internal_plugins):
if plugin_name not in excluding:
plugin_instance = self.plugin_registry[plugin_name]
if isinstance(plugin_instance, SpyderPluginV2):
can_close &= self.delete_plugin(
plugin_name, teardown=False, check_can_delete=False)
if not can_close and not close_immediately:
break
return can_close
[docs]
def get_plugin(self, plugin_name: str) -> SpyderPluginClass:
"""
Get a reference to a plugin instance by its name.
Parameters
----------
plugin_name: str
Name of the plugin to retrieve.
Returns
-------
plugin: SpyderPluginClass
The instance of the requested plugin.
Raises
------
SpyderAPIError
If the plugin name was not found in the registry.
"""
if plugin_name in self.plugin_registry:
plugin_instance = self.plugin_registry[plugin_name]
return plugin_instance
else:
raise SpyderAPIError(f'Plugin {plugin_name} was not found in '
'the registry')
[docs]
def set_plugin_enabled(self, plugin_name: str):
"""
Add a plugin name to the set of enabled plugins.
Parameters
----------
plugin_name: str
Name of the plugin to add.
"""
self.enabled_plugins |= {plugin_name}
[docs]
def is_plugin_enabled(self, plugin_name: str) -> bool:
"""
Determine if a given plugin is enabled and is going to be
loaded.
Parameters
----------
plugin_name: str
Name of the plugin to query.
Returns
-------
plugin_enabled: bool
True if the plugin is enabled and False if not.
"""
return plugin_name in self.enabled_plugins
[docs]
def is_plugin_available(self, plugin_name: str) -> bool:
"""
Determine if a given plugin was loaded and is available.
Parameters
----------
plugin_name: str
Name of the plugin to query.
Returns
-------
plugin_available: bool
True if the plugin is available and False if not.
"""
return self.plugin_availability.get(plugin_name, False)
[docs]
def reset(self):
"""Reset and empty the plugin registry."""
# 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]]]
# Dictionary that maps a plugin name to a list of the plugin names
# that the plugin depends on.
self.plugin_dependencies = {} # type: Dict[str, Dict[str, List[str]]]
# Plugin dictionary mapped by their names
self.plugin_registry = {} # type: Dict[str, SpyderPluginClass]
# Dictionary that maps a plugin name to its availability.
self.plugin_availability = {} # type: Dict[str, bool]
# Set that stores the plugin names of all Spyder 4 plugins.
self.old_plugins = set({}) # type: set[str]
# Set that stores the names of the plugins that are enabled
self.enabled_plugins = set({})
# Set that stores the names of the internal plugins
self.internal_plugins = set({})
# Set that stores the names of the external plugins
self.external_plugins = set({})
try:
self.sig_plugin_ready.disconnect()
except (TypeError, RuntimeError):
# Omit failures if there are no slots connected
pass
dependencies.DEPENDENCIES = []
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.
Parameters
----------
plugin_name: str
Name of the plugin to seek.
Returns
-------
is_contained: bool
If True, the plugin name is contained on the registry, False
otherwise.
"""
return plugin_name in self.plugin_registry
def __iter__(self):
return iter(self.plugin_registry)
PLUGIN_REGISTRY = SpyderPluginRegistry()