Skip to content

Commit

Permalink
Migrate almost all new-style plugins
Browse files Browse the repository at this point in the history
  • Loading branch information
andfoy committed Mar 8, 2021
1 parent 23eac45 commit d7c9629
Show file tree
Hide file tree
Showing 32 changed files with 712 additions and 746 deletions.
72 changes: 47 additions & 25 deletions spyder/api/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,39 +8,61 @@
Spyder API helper decorators.
"""

# Standard library imports
import functools
from typing import Callable, Type, Any, Optional, Union, List
import inspect

def configuration_observer(cls):
"""
Enable a class to recieve configuration update notifications.
# Local imports
from spyder.config.types import ConfigurationKey

This class decorator maps completion listener configuration sections and
options to their corresponding class methods.
"""
cls._configuration_listeners = {}
for method_name in dir(cls):
method = getattr(cls, method_name)
if hasattr(method, '_conf_listen'):
section, option = method._conf_listen
sect_ion_listeners = cls.configuration_listeners.get(section, {})
option_listeners = section_listeners.get(option, [])
option_listeners.append(method_name)
section_listeners[option] = option_listeners
cls._configuration_listeners[section] = section_listeners
return cls


def on_conf_change(section=None, option=None):

ConfigurationKeyList = List[ConfigurationKey]
ConfigurationKeyOrList = Union[ConfigurationKeyList, ConfigurationKey]


def on_conf_change(func: Callable = None,
section: Optional[str] = None,
option: Optional[ConfigurationKeyOrList] = None) -> Callable:
"""
Method decorator used to handle changes on the configuration option
`option` of the section `section`.
The methods that use this decorator must have the following signature
`def method(value): ...`
`def method(self, value)` when observing a single value or the whole
section and `def method(self, option, value): ...` when observing
multiple values.
Parameters
----------
func: Callable
Method to decorate. Given by default when applying the decorator.
section: Optional[str]
Name of the configuration whose option is going to be observed for
changes. If None, then the `CONF_SECTION` attribute of the class
where the method is defined is used.
option: Optional[ConfigurationKeyOrList]
Name/tuple of the option to observe or a list of name/tuples if the
method expects updates from multiple keys. If None, then all changes
on the specified section are observed.
Returns
-------
func: Callable
The same method that was given as input.
"""
if func is None:
return functools.partial(
on_conf_change, section=section, option=option)

if option is None:
option = '__section'

def wrapper(func):
func._conf_listen = (section, option)
return func
return wrapper
info = []
if isinstance(option, list):
info = [(section, opt) for opt in option]
else:
info = [(section, option)]

func._conf_listen = info
return func
172 changes: 162 additions & 10 deletions spyder/api/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,189 @@
"""

# Standard library imports
from typing import Any
import logging
from typing import Any, Union, Optional

# Local imports
from spyder.api.decorators import configuration_observer
from spyder.config.manager import CONF
from spyder.config.types import ConfigurationKey
from spyder.config.user import NoDefault


@configuration_observer
class SpyderConfigurationObserver:
logger = logging.getLogger(__name__)

BasicTypes = Union[bool, int, str, tuple, list, dict]


class SpyderConfigurationAccessor:
"""
Mixin used to access options stored in the Spyder configuration.
"""

# Name of the configuration section that's going to be
# used to record the object's permanent data in Spyder
# config system (i.e. in spyder.ini)
CONF_SECTION = None

def get_conf(self,
option: ConfigurationKey,
default: Union[NoDefault, BasicTypes] = NoDefault,
section: Optional[str] = None):
"""
Get an option from Spyder configuration system.
Parameters
----------
option: ConfigurationKey
Name/Tuple path of the option to get its value from.
default: Union[NoDefault, BasicTypes]
Fallback value to return if the option is not found on the
configuration system.
section: str
Section in the configuration system, e.g. `shortcuts`. If None,
then the value of `CONF_SECTION` is used.
Returns
-------
value: BasicTypes
Value of the option in the configuration section.
Raises
------
spyder.py3compat.configparser.NoOptionError
If the option does not exist in the configuration under the given
section and the default value is NoDefault.
"""
section = self.CONF_SECTION if section is None else section
if section is None:
raise AttributeError(
'A SpyderConfigurationAccessor must define a `CONF_SECTION` '
'class attribute!'
)

return CONF.get(section, option, default)

def set_conf(self,
option: ConfigurationKey,
value: BasicTypes,
section: Optional[str] = None):
"""
Set an option in the Spyder configuration system.
Parameters
----------
option: ConfigurationKey
Name/Tuple path of the option to set its value.
value: BasicTypes
Value to set on the configuration system.
section: Optional[str]
Section in the configuration system, e.g. `shortcuts`. If None,
then the value of `CONF_SECTION` is used.
"""
section = self.CONF_SECTION if section is None else section
if section is None:
raise AttributeError(
'A SpyderConfigurationAccessor must define a `CONF_SECTION` '
'class attribute!'
)
CONF.set(section, option, value)


def remove_conf(self,
option: ConfigurationKey,
section: Optional[str] = None):
"""
Remove an option in the Spyder configuration system.
Parameters
----------
option: ConfigurationKey
Name/Tuple path of the option to set its value.
section: Optional[str]
Section in the configuration system, e.g. `shortcuts`. If None,
then the value of `CONF_SECTION` is used.
"""
section = self.CONF_SECTION if section is None else section
if section is None:
raise AttributeError(
'A SpyderConfigurationAccessor must define a `CONF_SECTION` '
'class attribute!'
)
CONF.remove_option(section, option)


class SpyderConfigurationObserver(SpyderConfigurationAccessor):
"""
Concrete implementation of the protocol
:class:`spyder.config.types.ConfigurationObserver`.
This mixin enables a class to recieve configuration updates seamlessly,
This mixin enables a class to receive configuration updates seamlessly,
by registering methods using the
:function:`spyder.api.decorators.on_conf_change` decorator, which recieves
:function:`spyder.api.decorators.on_conf_change` decorator, which receives
a configuration section and option to observe.
When a change occurs on any of the registered configuration options,
the corresponding registered method is called with the new value.
"""

def __init__(self):
if self.CONF_SECTION is None:
raise AttributeError(
'A SpyderConfigurationObserver must define a `CONF_SECTION` '
'class attribute!'
)

self._configuration_listeners = {}
self._multi_option_listeners = set({})
self._gather_observers()
self._merge_none_observers()

# Register class to listen for changes in all the registered options
for section in self._configuration_listeners:
section = self.CONF_SECTION if section is None else section
observed_options = self._configuration_listeners[section]
for option in observed_options:
logger.debug(f'{self} is observing {option} '
f'in section {section}')
CONF.observe_configuration(self, section, option)

def __del__(self):
# Remove object from the configuration observer
CONF.unobserve_configuration(self)

def _gather_observers(self):
"""Gather all the methods decorated with `on_conf_change`."""
for method_name in dir(self):
method = getattr(self, method_name, None)
if hasattr(method, '_conf_listen'):
info = method._conf_listen
if len(info) > 1:
self._multi_option_listeners |= {method_name}

for section, option in info:
section_listeners = self._configuration_listeners.get(
section, {})
option_listeners = section_listeners.get(option, [])
option_listeners.append(method_name)
section_listeners[option] = option_listeners
self._configuration_listeners[section] = section_listeners

def _merge_none_observers(self):
"""Replace observers that declared section as None by CONF_SECTION."""
default_selectors = self._configuration_listeners.get(None, {})
section_selectors = self._configuration_listeners.get(
self.CONF_SECTION, {})

for option in default_selectors:
default_option_receivers = default_selectors.get(option, [])
section_option_receivers = section_selectors.get(option, [])
merged_receivers = (
default_option_receivers + section_option_receivers)
section_selectors[option] = merged_receivers

self._configuration_listeners[self.CONF_SECTION] = section_selectors
self._configuration_listeners.pop(None, None)

def on_configuration_change(self, option: ConfigurationKey, section: str,
value: Any):
"""
Expand All @@ -58,8 +207,11 @@ def on_configuration_change(self, option: ConfigurationKey, section: str,
value: Any
New value of the configuration option that produced the event.
"""
section_recievers = self._configuration_listeners.get(section, {})
option_recievers = section_recievers.get(option, [])
for receiver in option_recievers:
section_receivers = self._configuration_listeners.get(section, {})
option_receivers = section_receivers.get(option, [])
for receiver in option_receivers:
method = getattr(self, receiver)
method(value)
if receiver in self._multi_option_listeners:
method(option, value)
else:
method(value)
Loading

6 comments on commit d7c9629

@novaya
Copy link
Contributor

@novaya novaya commented on d7c9629 Mar 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ccordoba12 @andfoy After this change we see date column in project plugin. Was it intentional? Also code is not cleaned well. We have in many places "options" argument in function docstrings.

new

@ccordoba12
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After this change we see date column in project plugin

No, it's not, but I can't reproduce this problem with clean settings. Please open an issue about it, explaining how you got that error.

We have in many place "options" argument in function docstring.

Could you be more specific? I couldn't easily detect instances where options was left in docstrings. Please also open an issue about it.

@novaya
Copy link
Contributor

@novaya novaya commented on d7c9629 Mar 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ccordoba12 I just added PR #14963 where I removed some of those. And latest master projects plugin has column with dates. You don't have it? Did you run latest master?

@ccordoba12
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ccordoba12 I just added PR #14963 where I removed some of those.

Ok, thanks a lot for that!

You don't have it?

No, I'm not seeing it.

Did you run latest master?

Yep.

@novaya
Copy link
Contributor

@novaya novaya commented on d7c9629 Mar 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ccordoba12 After closing and reopening the project I don't have date column anymore. So I don't know why I had it and why I don't have it now. If I will be able reproduce it I will let you know.

@ccordoba12
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, thanks for feedback! I was able to reproduce the error: it happens when you close Spyder with an active project and then reopen it again.

I'll fix it right away.

Please sign in to comment.