File: C:/Users/fred/anaconda3/Lib/site-packages/spyder/config/user.py
# -*- coding: utf-8 -*-
#
# Copyright © Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see spyder/__init__.py for details)
"""
This module provides user configuration file management features for Spyder.
It is based on the ConfigParser module present in the standard library.
"""
# Standard library imports
import ast
import configparser as cp
import copy
import io
import os
import os.path as osp
import re
import shutil
import time
# Local imports
from spyder.config.base import get_conf_path, get_module_source_path
from spyder.py3compat import is_text_string, to_text_string
from spyder.utils.programs import check_version
# ============================================================================
# Auxiliary classes
# ============================================================================
class NoDefault:
pass
# ============================================================================
# Defaults class
# ============================================================================
class DefaultsConfig(cp.ConfigParser, object):
"""
Class used to save defaults to a file and as UserConfig base class.
"""
def __init__(self, name, path):
"""
Class used to save defaults to a file and as UserConfig base class.
"""
super(DefaultsConfig, self).__init__(interpolation=None)
self._name = name
self._path = path
if not osp.isdir(osp.dirname(self._path)):
os.makedirs(osp.dirname(self._path))
def _write(self, fp):
"""
Write method for Python 2.
The one from configparser fails for non-ascii Windows accounts.
"""
if self._defaults:
fp.write('[{}]\n'.format(cp.DEFAULTSECT))
for (key, value) in self._defaults.items():
value_plus_end_of_line = str(value).replace('\n', '\n\t')
fp.write('{} = {}\n'.format(key, value_plus_end_of_line))
fp.write('\n')
for section in self._sections:
fp.write('[{}]\n'.format(section))
for (key, value) in self._sections[section].items():
if key == '__name__':
continue
if (value is not None) or (self._optcre == self.OPTCRE):
value = to_text_string(value)
value_plus_end_of_line = value.replace('\n', '\n\t')
key = ' = '.join((key, value_plus_end_of_line))
fp.write('{}\n'.format(key))
fp.write('\n')
def _set(self, section, option, value, verbose):
"""Set method."""
if not self.has_section(section):
self.add_section(section)
if not is_text_string(value):
value = repr(value)
if verbose:
text = '[{}][{}] = {}'.format(section, option, value)
print(text) # spyder: test-skip
super(DefaultsConfig, self).set(section, option, value)
def _save(self):
"""Save config into the associated .ini file."""
fpath = self.get_config_fpath()
def _write_file(fpath):
with io.open(fpath, 'w', encoding='utf-8') as configfile:
self.write(configfile)
# See spyder-ide/spyder#1086 and spyder-ide/spyder#1242 for background
# on why this method contains all the exception handling.
try:
# The "easy" way
_write_file(fpath)
except EnvironmentError:
try:
# The "delete and sleep" way
if osp.isfile(fpath):
os.remove(fpath)
time.sleep(0.05)
_write_file(fpath)
except Exception as e:
print('Failed to write user configuration file to disk, with '
'the exception shown below') # spyder: test-skip
print(e) # spyder: test-skip
def get_config_fpath(self):
"""Return the ini file where this configuration is stored."""
path = self._path
config_file = osp.join(path, '{}.ini'.format(self._name))
return config_file
def set_defaults(self, defaults):
"""Set default values and save to defaults folder location."""
for section, options in defaults:
for option in options:
new_value = options[option]
self._set(section, option, new_value, verbose=False)
# ============================================================================
# User config class
# ============================================================================
class UserConfig(DefaultsConfig):
"""
UserConfig class, based on ConfigParser.
Parameters
----------
name: str
Name of the config
path: str
Configuration file will be saved in path/%name%.ini
defaults: {} or [(str, {}),]
Dictionary containing options *or* list of tuples (sec_name, options)
load: bool
If a previous configuration file is found, load will take the values
from this existing file, instead of using default values.
version: str
version of the configuration file in 'major.minor.micro' format.
backup: bool
A backup will be created on version changes and on initial setup.
raw_mode: bool
If `True` do not apply any automatic conversion on values read from
the configuration.
remove_obsolete: bool
If `True`, values that were removed from the configuration on version
change, are removed from the saved configuration file.
Notes
-----
The 'get' and 'set' arguments number and type differ from the overriden
methods. 'defaults' is an attribute and not a method.
"""
DEFAULT_SECTION_NAME = 'main'
def __init__(self, name, path, defaults=None, load=True, version=None,
backup=False, raw_mode=False, remove_obsolete=False,
external_plugin=False):
"""UserConfig class, based on ConfigParser."""
super(UserConfig, self).__init__(name=name, path=path)
self._load = load
self._version = self._check_version(version)
self._backup = backup
self._raw = 1 if raw_mode else 0
self._remove_obsolete = remove_obsolete
self._external_plugin = external_plugin
self._module_source_path = get_module_source_path('spyder')
self._defaults_folder = 'defaults'
self._backup_folder = 'backups'
self._backup_suffix = '.bak'
self._defaults_name_prefix = 'defaults'
# This attribute is overriding a method from cp.ConfigParser
self.defaults = self._check_defaults(defaults)
if backup:
self._make_backup()
if load:
# If config file already exists, it overrides Default options
previous_fpath = self.get_previous_config_fpath()
self._load_from_ini(previous_fpath)
old_version = self.get_version(version)
self._old_version = old_version
# Save new defaults
self._save_new_defaults(self.defaults)
# Updating defaults only if major/minor version is different
if (self._get_minor_version(version)
!= self._get_minor_version(old_version)):
if backup:
self._make_backup(version=old_version)
self.apply_configuration_patches(old_version=old_version)
# Remove deprecated options if major version has changed
if remove_obsolete:
self._remove_deprecated_options(old_version)
# Set new version number
self.set_version(version, save=False)
if defaults is None:
# If no defaults are defined set .ini file settings as default
self.set_as_defaults()
# --- Helpers and checkers
# ------------------------------------------------------------------------
@staticmethod
def _get_minor_version(version):
"""Return the 'major.minor' components of the version."""
return version[:version.rfind('.')]
@staticmethod
def _get_major_version(version):
"""Return the 'major' component of the version."""
return version[:version.find('.')]
@staticmethod
def _check_version(version):
"""Check version is compliant with format."""
regex_check = re.match(r'^(\d+).(\d+).(\d+)$', version)
if version is not None and regex_check is None:
raise ValueError('Version number {} is incorrect - must be in '
'major.minor.micro format'.format(version))
return version
def _check_defaults(self, defaults):
"""Check if defaults are valid and update defaults values."""
if defaults is None:
defaults = [(self.DEFAULT_SECTION_NAME, {})]
elif isinstance(defaults, dict):
defaults = [(self.DEFAULT_SECTION_NAME, defaults)]
elif isinstance(defaults, list):
# Check is a list of tuples with strings and dictionaries
for sec, options in defaults:
assert is_text_string(sec)
assert isinstance(options, dict)
for opt, _ in options.items():
assert is_text_string(opt)
else:
raise ValueError('`defaults` must be a dict or a list of tuples!')
# This attribute is overriding a method from cp.ConfigParser
self.defaults = defaults
if defaults is not None:
self.reset_to_defaults(save=False)
return defaults
@classmethod
def _check_section_option(cls, section, option):
"""Check section and option types."""
if section is None:
section = cls.DEFAULT_SECTION_NAME
elif not is_text_string(section):
raise RuntimeError("Argument 'section' must be a string")
if not is_text_string(option):
raise RuntimeError("Argument 'option' must be a string")
return section
def _make_backup(self, version=None, old_version=None):
"""
Make a backup of the configuration file.
If `old_version` is `None` a normal backup is made. If `old_version`
is provided, then the backup was requested for minor version changes
and appends the version number to the backup file.
"""
fpath = self.get_config_fpath()
fpath_backup = self.get_backup_fpath_from_version(
version=version, old_version=old_version)
path = os.path.dirname(fpath_backup)
if not osp.isdir(path):
os.makedirs(path)
try:
shutil.copyfile(fpath, fpath_backup)
except IOError:
pass
def _load_from_ini(self, fpath):
"""Load config from the associated .ini file found at `fpath`."""
try:
self.read(fpath, encoding='utf-8')
except cp.MissingSectionHeaderError:
error_text = 'Warning: File contains no section headers.'
print(error_text) # spyder: test-skip
def _load_old_defaults(self, old_version):
"""Read old defaults."""
old_defaults = cp.ConfigParser()
path, name = self.get_defaults_path_name_from_version(old_version)
old_defaults.read(osp.join(path, name + '.ini'))
return old_defaults
def _save_new_defaults(self, defaults):
"""Save new defaults."""
path, name = self.get_defaults_path_name_from_version()
new_defaults = DefaultsConfig(name=name, path=path)
if not osp.isfile(new_defaults.get_config_fpath()):
new_defaults.set_defaults(defaults)
new_defaults._save()
def _update_defaults(self, defaults, old_version, verbose=False):
"""Update defaults after a change in version."""
old_defaults = self._load_old_defaults(old_version)
for section, options in defaults:
for option in options:
new_value = options[option]
try:
old_val = old_defaults.get(section, option)
except (cp.NoSectionError, cp.NoOptionError):
old_val = None
if old_val is None or to_text_string(new_value) != old_val:
self._set(section, option, new_value, verbose)
def _remove_deprecated_options(self, old_version):
"""
Remove options which are present in the .ini file but not in defaults.
"""
old_defaults = self._load_old_defaults(old_version)
for section in old_defaults.sections():
for option, _ in old_defaults.items(section, raw=self._raw):
if self.get_default(section, option) is NoDefault:
try:
self.remove_option(section, option)
if len(self.items(section, raw=self._raw)) == 0:
self.remove_section(section)
except cp.NoSectionError:
self.remove_section(section)
# --- Compatibility API
# ------------------------------------------------------------------------
def get_previous_config_fpath(self):
"""Return the last configuration file used if found."""
return self.get_config_fpath()
def get_config_fpath_from_version(self, version=None):
"""
Return the configuration path for given version.
If no version is provided, it returns the current file path.
"""
return self.get_config_fpath()
def get_backup_fpath_from_version(self, version=None, old_version=None):
"""
Get backup location based on version.
`old_version` can be used for checking compatibility whereas `version`
relates to adding the version to the file name.
To be overridden if versions changed backup location.
"""
fpath = self.get_config_fpath()
path = osp.join(osp.dirname(fpath), self._backup_folder)
new_fpath = osp.join(path, osp.basename(fpath))
if version is None:
backup_fpath = '{}{}'.format(new_fpath, self._backup_suffix)
else:
backup_fpath = "{}-{}{}".format(new_fpath, version,
self._backup_suffix)
return backup_fpath
def get_defaults_path_name_from_version(self, old_version=None):
"""
Get defaults location based on version.
To be overridden if versions changed defaults location.
"""
version = old_version if old_version else self._version
defaults_path = osp.join(osp.dirname(self.get_config_fpath()),
self._defaults_folder)
name = '{}-{}-{}'.format(
self._defaults_name_prefix,
self._name,
version,
)
if not osp.isdir(defaults_path):
os.makedirs(defaults_path)
return defaults_path, name
def apply_configuration_patches(self, old_version=None):
"""
Apply any patch to configuration values on version changes.
To be overridden if patches to configuration values are needed.
"""
pass
# --- Public API
# ------------------------------------------------------------------------
def get_version(self, version='0.0.0'):
"""Return configuration (not application!) version."""
return self.get(self.DEFAULT_SECTION_NAME, 'version', version)
def set_version(self, version='0.0.0', save=True):
"""Set configuration (not application!) version."""
version = self._check_version(version)
self.set(self.DEFAULT_SECTION_NAME, 'version', version, save=save)
def reset_to_defaults(self, save=True, verbose=False, section=None):
"""Reset config to Default values."""
for sec, options in self.defaults:
if section == None or section == sec:
for option in options:
value = options[option]
self._set(sec, option, value, verbose)
if save:
self._save()
def set_as_defaults(self):
"""Set defaults from the current config."""
self.defaults = []
for section in self.sections():
secdict = {}
for option, value in self.items(section, raw=self._raw):
try:
value = ast.literal_eval(value)
except (SyntaxError, ValueError):
pass
secdict[option] = value
self.defaults.append((section, secdict))
def get_default(self, section, option):
"""
Get default value for a given `section` and `option`.
This is useful for type checking in `get` method.
"""
section = self._check_section_option(section, option)
for sec, options in self.defaults:
if sec == section:
if option in options:
value = options[option]
break
else:
value = NoDefault
return value
def get(self, section, option, default=NoDefault):
"""
Get an option.
Parameters
----------
section: str
Section name. If `None` is provide use the default section name.
option: str
Option name for `section`.
default:
Default value (if not specified, an exception will be raised if
option doesn't exist).
"""
section = self._check_section_option(section, option)
if not self.has_section(section):
if default is NoDefault:
raise cp.NoSectionError(section)
else:
self.add_section(section)
if not self.has_option(section, option):
if default is NoDefault:
raise cp.NoOptionError(option, section)
else:
self.set(section, option, default)
return default
value = super(UserConfig, self).get(section, option, raw=self._raw)
default_value = self.get_default(section, option)
if isinstance(default_value, bool):
value = ast.literal_eval(value)
elif isinstance(default_value, float):
value = float(value)
elif isinstance(default_value, int):
value = int(value)
elif is_text_string(default_value):
pass
else:
try:
# Lists, tuples, ...
value = ast.literal_eval(value)
except (SyntaxError, ValueError):
pass
return value
def set_default(self, section, option, default_value):
"""
Set Default value for a given `section`, `option`.
If no defaults exist, no default is created. To be able to set
defaults, a call to set_as_defaults is needed to create defaults
based on current values.
"""
section = self._check_section_option(section, option)
for sec, options in self.defaults:
if sec == section:
options[option] = default_value
def set(self, section, option, value, verbose=False, save=True):
"""
Set an `option` on a given `section`.
If section is None, the `option` is added to the default section.
"""
section = self._check_section_option(section, option)
default_value = self.get_default(section, option)
if default_value is NoDefault:
default_value = value
self.set_default(section, option, default_value)
if isinstance(default_value, bool):
value = bool(value)
elif isinstance(default_value, float):
value = float(value)
elif isinstance(default_value, int):
value = int(value)
elif not is_text_string(default_value):
value = repr(value)
self._set(section, option, value, verbose)
if save:
self._save()
def remove_section(self, section):
"""Remove `section` and all options within it."""
super(UserConfig, self).remove_section(section)
self._save()
def remove_option(self, section, option):
"""Remove `option` from `section`."""
super(UserConfig, self).remove_option(section, option)
self._save()
def cleanup(self):
"""Remove .ini file associated to config."""
os.remove(self.get_config_fpath())
def to_list(self):
"""
Return in list format.
The format is [('section1', {'opt-1': value, ...}),
('section2', {'opt-2': othervalue, ...}), ...]
"""
new_defaults = []
self._load_from_ini(self.get_config_fpath())
for section in self._sections:
sec_data = {}
for (option, _) in self.items(section):
sec_data[option] = self.get(section, option)
new_defaults.append((section, sec_data))
return new_defaults
class SpyderUserConfig(UserConfig):
def get_previous_config_fpath(self):
"""
Override method.
Return the last configuration file used if found.
"""
fpath = self.get_config_fpath()
# We don't need to add the contents of the old spyder.ini to
# the configuration of external plugins. This was the cause
# of part two (the shortcut conflicts) of issue
# spyder-ide/spyder#11132
if self._external_plugin:
previous_paths = [fpath]
else:
previous_paths = [
# >= 51.0.0
fpath,
# < 51.0.0
os.path.join(get_conf_path(), 'spyder.ini'),
]
for fpath in previous_paths:
if osp.isfile(fpath):
break
return fpath
def get_config_fpath_from_version(self, version=None):
"""
Override method.
Return the configuration path for given version.
If no version is provided, it returns the current file path.
"""
if version is None or self._external_plugin:
fpath = self.get_config_fpath()
elif check_version(version, '51.0.0', '<'):
fpath = osp.join(get_conf_path(), 'spyder.ini')
else:
fpath = self.get_config_fpath()
return fpath
def get_backup_fpath_from_version(self, version=None, old_version=None):
"""
Override method.
Make a backup of the configuration file.
"""
if old_version and check_version(old_version, '51.0.0', '<'):
name = 'spyder.ini'
fpath = os.path.join(get_conf_path(), name)
if version is None:
backup_fpath = "{}{}".format(fpath, self._backup_suffix)
else:
backup_fpath = "{}-{}{}".format(fpath, version,
self._backup_suffix)
else:
super_class = super(SpyderUserConfig, self)
backup_fpath = super_class.get_backup_fpath_from_version(
version, old_version)
return backup_fpath
def get_defaults_path_name_from_version(self, old_version=None):
"""
Override method.
Get defaults location based on version.
"""
if old_version:
if check_version(old_version, '51.0.0', '<'):
name = '{}-{}'.format(self._defaults_name_prefix, old_version)
path = osp.join(get_conf_path(), 'defaults')
else:
super_class = super(SpyderUserConfig, self)
path, name = super_class.get_defaults_path_name_from_version(
old_version)
else:
super_class = super(SpyderUserConfig, self)
path, name = super_class.get_defaults_path_name_from_version()
return path, name
def apply_configuration_patches(self, old_version=None):
"""
Override method.
Apply any patch to configuration values on version changes.
"""
self._update_defaults(self.defaults, old_version)
if self._external_plugin:
return
if old_version and check_version(old_version, '44.1.0', '<'):
run_lines = to_text_string(self.get('ipython_console',
'startup/run_lines'))
if run_lines is not NoDefault:
run_lines = run_lines.replace(',', '; ')
self.set('ipython_console', 'startup/run_lines', run_lines)
class MultiUserConfig(object):
"""
Multiuser config class which emulates the basic UserConfig interface.
This class provides the same basic interface as UserConfig but allows
splitting the configuration sections and options among several files.
The `name` is now a `name_map` where the sections and options per file name
are defined.
"""
DEFAULT_FILE_NAME = 'spyder'
def __init__(self, name_map, path, defaults=None, load=True, version=None,
backup=False, raw_mode=False, remove_obsolete=False,
external_plugin=False):
"""Multi user config class based on UserConfig class."""
self._name_map = self._check_name_map(name_map)
self._path = path
self._defaults = defaults
self._load = load
self._version = version
self._backup = backup
self._raw_mode = 1 if raw_mode else 0
self._remove_obsolete = remove_obsolete
self._external_plugin = external_plugin
self._configs_map = {}
self._config_defaults_map = self._get_defaults_for_name_map(defaults,
name_map)
self._config_kwargs = {
'path': path,
'defaults': defaults,
'load': load,
'version': version,
'backup': backup,
'raw_mode': raw_mode,
'remove_obsolete': False, # This will be handled later on if True
'external_plugin': external_plugin
}
for name in name_map:
defaults = self._config_defaults_map.get(name)
mod_kwargs = {
'name': name,
'defaults': defaults,
}
new_kwargs = self._config_kwargs.copy()
new_kwargs.update(mod_kwargs)
config_class = self.get_config_class()
self._configs_map[name] = config_class(**new_kwargs)
# Remove deprecated options if major version has changed
default_config = self._configs_map.get(self.DEFAULT_FILE_NAME)
major_ver = default_config._get_major_version(version)
major_old_ver = default_config._get_major_version(
default_config._old_version)
# Now we can apply remove_obsolete
if remove_obsolete or major_ver != major_old_ver:
for _, config in self._configs_map.items():
config._remove_deprecated_options(config._old_version)
def _get_config(self, section, option):
"""Get the correct configuration based on section and option."""
# Check the filemap first
name = self._get_name_from_map(section, option)
config_value = self._configs_map.get(name, None)
if config_value is None:
config_value = self._configs_map[self.DEFAULT_FILE_NAME]
return config_value
def _check_name_map(self, name_map):
"""Check `name_map` follows the correct format."""
# Check section option paris are not repeated
sections_options = []
for _, sec_opts in name_map.items():
for section, options in sec_opts:
for option in options:
sec_opt = (section, option)
if sec_opt not in sections_options:
sections_options.append(sec_opt)
else:
error_msg = (
'Different files are holding the same '
'section/option: "{}/{}"!'.format(section, option)
)
raise ValueError(error_msg)
return name_map
@staticmethod
def _get_section_from_defaults(defaults, section):
"""Get the section contents from the defaults."""
for sec, options in defaults:
if section == sec:
value = options
break
else:
raise ValueError('section "{}" not found!'.format(section))
return value
@staticmethod
def _get_option_from_defaults(defaults, section, option):
"""Get the section,option value from the defaults."""
value = NoDefault
for sec, options in defaults:
if section == sec:
value = options.get(option, NoDefault)
break
return value
@staticmethod
def _remove_section_from_defaults(defaults, section):
"""Remove section from defaults."""
idx_remove = None
for idx, (sec, _) in enumerate(defaults):
if section == sec:
idx_remove = idx
break
if idx_remove is not None:
defaults.pop(idx)
@staticmethod
def _remove_option_from_defaults(defaults, section, option):
"""Remove section,option from defaults."""
for sec, options in defaults:
if section == sec:
if option in options:
options.pop(option)
break
def _get_name_from_map(self, section=None, option=None):
"""
Search for section and option on the name_map and return the name.
"""
for name, sec_opts in self._name_map.items():
# Ignore the main section
default_sec_name = self._configs_map.get(name).DEFAULT_SECTION_NAME
if name == default_sec_name:
continue
for sec, options in sec_opts:
if sec == section:
if len(options) == 0:
return name
else:
for opt in options:
if opt == option:
return name
@classmethod
def _get_defaults_for_name_map(cls, defaults, name_map):
"""Split the global defaults using the name_map."""
name_map_config = {}
defaults_copy = copy.deepcopy(defaults)
for name, sec_opts in name_map.items():
default_map_for_name = []
if len(sec_opts) == 0:
name_map_config[name] = defaults_copy
else:
for section, options in sec_opts:
if len(options) == 0:
# Use all on section
sec = cls._get_section_from_defaults(defaults_copy,
section)
# Remove section from defaults
cls._remove_section_from_defaults(defaults_copy,
section)
else:
# Iterate and pop!
sec = {}
for opt in options:
val = cls._get_option_from_defaults(defaults_copy,
section, opt)
if val is not NoDefault:
sec[opt] = val
# Remove option from defaults
cls._remove_option_from_defaults(defaults_copy,
section, opt)
# Add to config map
default_map_for_name.append((section, sec))
name_map_config[name] = default_map_for_name
return name_map_config
def get_config_class(self):
"""Return the UserConfig class to use."""
return SpyderUserConfig
def sections(self):
"""Return all sections of the configuration file."""
sections = set()
for _, config in self._configs_map.items():
for section in config.sections():
sections.add(section)
return list(sorted(sections))
def items(self, section):
"""Return all the items option/values for the given section."""
config = self._get_config(section, None)
if config is None:
config = self._configs_map[self.DEFAULT_FILE_NAME]
if config.has_section(section):
return config.items(section=section)
else:
return None
def options(self, section):
"""Return all the options for the given section."""
config = self._get_config(section, None)
return config.options(section=section)
def get_default(self, section, option):
"""
Get Default value for a given `section` and `option`.
This is useful for type checking in `get` method.
"""
config = self._get_config(section, option)
if config is None:
config = self._configs_map[self.DEFAULT_FILE_NAME]
return config.get_default(section, option)
def get(self, section, option, default=NoDefault):
"""
Get an `option` on a given `section`.
If section is None, the `option` is requested from default section.
"""
config = self._get_config(section, option)
if config is None:
config = self._configs_map[self.DEFAULT_FILE_NAME]
return config.get(section=section, option=option, default=default)
def set(self, section, option, value, verbose=False, save=True):
"""
Set an `option` on a given `section`.
If section is None, the `option` is added to the default section.
"""
config = self._get_config(section, option)
config.set(section=section, option=option, value=value,
verbose=verbose, save=save)
def reset_to_defaults(self, section=None):
"""Reset configuration to Default values."""
for _, config in self._configs_map.items():
config.reset_to_defaults(section=section)
def remove_section(self, section):
"""Remove `section` and all options within it."""
config = self._get_config(section, None)
config.remove_section(section)
def remove_option(self, section, option):
"""Remove `option` from `section`."""
config = self._get_config(section, option)
config.remove_option(section, option)
def cleanup(self):
"""Remove .ini files associated to configurations."""
for _, config in self._configs_map.items():
os.remove(config.get_config_fpath())
class PluginConfig(UserConfig):
"""Plugin configuration handler."""
class PluginMultiConfig(MultiUserConfig):
"""Plugin configuration handler with multifile support."""
def get_config_class(self):
return PluginConfig