1# -*- coding: utf-8 -*- 2# 3# Copyright © Spyder Project Contributors 4# Licensed under the terms of the MIT License 5# (see spyder/__init__.py for details) 6 7""" 8This module provides user configuration file management features for Spyder 9 10It's based on the ConfigParser module (present in the standard library). 11""" 12 13from __future__ import print_function 14 15# Std imports 16import ast 17import os 18import re 19import os.path as osp 20import shutil 21import time 22 23# Local imports 24from spyder.config.base import (get_conf_path, get_home_dir, 25 get_module_source_path, TEST) 26from spyder.utils.programs import check_version 27from spyder.py3compat import configparser as cp 28from spyder.py3compat import PY2, is_text_string, to_text_string 29 30# Std imports for Python 2 31if PY2: 32 import codecs 33 34 35#============================================================================== 36# Auxiliary classes 37#============================================================================== 38class NoDefault: 39 pass 40 41 42#============================================================================== 43# Defaults class 44#============================================================================== 45class DefaultsConfig(cp.ConfigParser): 46 """ 47 Class used to save defaults to a file and as base class for 48 UserConfig 49 """ 50 def __init__(self, name, subfolder): 51 if PY2: 52 cp.ConfigParser.__init__(self) 53 else: 54 cp.ConfigParser.__init__(self, interpolation=None) 55 self.name = name 56 self.subfolder = subfolder 57 58 def _write(self, fp): 59 """ 60 Private write method for Python 2 61 The one from configparser fails for non-ascii Windows accounts 62 """ 63 if self._defaults: 64 fp.write("[%s]\n" % cp.DEFAULTSECT) 65 for (key, value) in self._defaults.items(): 66 fp.write("%s = %s\n" % (key, str(value).replace('\n', '\n\t'))) 67 fp.write("\n") 68 for section in self._sections: 69 fp.write("[%s]\n" % section) 70 for (key, value) in self._sections[section].items(): 71 if key == "__name__": 72 continue 73 if (value is not None) or (self._optcre == self.OPTCRE): 74 value = to_text_string(value) 75 key = " = ".join((key, value.replace('\n', '\n\t'))) 76 fp.write("%s\n" % (key)) 77 fp.write("\n") 78 79 def _set(self, section, option, value, verbose): 80 """ 81 Private set method 82 """ 83 if not self.has_section(section): 84 self.add_section( section ) 85 if not is_text_string(value): 86 value = repr( value ) 87 if verbose: 88 print('%s[ %s ] = %s' % (section, option, value)) # spyder: test-skip 89 cp.ConfigParser.set(self, section, option, value) 90 91 def _save(self): 92 """ 93 Save config into the associated .ini file 94 """ 95 # Don't save settings if we are on testing mode 96 if TEST: 97 return 98 99 # See Issue 1086 and 1242 for background on why this 100 # method contains all the exception handling. 101 fname = self.filename() 102 103 def _write_file(fname): 104 if PY2: 105 # Python 2 106 with codecs.open(fname, 'w', encoding='utf-8') as configfile: 107 self._write(configfile) 108 else: 109 # Python 3 110 with open(fname, 'w', encoding='utf-8') as configfile: 111 self.write(configfile) 112 113 try: # the "easy" way 114 _write_file(fname) 115 except IOError: 116 try: # the "delete and sleep" way 117 if osp.isfile(fname): 118 os.remove(fname) 119 time.sleep(0.05) 120 _write_file(fname) 121 except Exception as e: 122 print("Failed to write user configuration file.") # spyder: test-skip 123 print("Please submit a bug report.") # spyder: test-skip 124 raise(e) 125 126 def filename(self): 127 """Defines the name of the configuration file to use.""" 128 # Needs to be done this way to be used by the project config. 129 # To fix on a later PR 130 self._filename = getattr(self, '_filename', None) 131 self._root_path = getattr(self, '_root_path', None) 132 133 if self._filename is None and self._root_path is None: 134 return self._filename_global() 135 else: 136 return self._filename_projects() 137 138 def _filename_projects(self): 139 """Create a .ini filename located in the current project directory. 140 This .ini files stores the specific project preferences for each 141 project created with spyder. 142 """ 143 return osp.join(self._root_path, self._filename) 144 145 def _filename_global(self): 146 """Create a .ini filename located in user home directory. 147 This .ini files stores the global spyder preferences. 148 """ 149 if self.subfolder is None: 150 config_file = osp.join(get_home_dir(), '.%s.ini' % self.name) 151 return config_file 152 else: 153 folder = get_conf_path() 154 # Save defaults in a "defaults" dir of .spyder2 to not pollute it 155 if 'defaults' in self.name: 156 folder = osp.join(folder, 'defaults') 157 if not osp.isdir(folder): 158 os.mkdir(folder) 159 config_file = osp.join(folder, '%s.ini' % self.name) 160 return config_file 161 162 def set_defaults(self, defaults): 163 for section, options in defaults: 164 for option in options: 165 new_value = options[ option ] 166 self._set(section, option, new_value, False) 167 168 169#============================================================================== 170# User config class 171#============================================================================== 172class UserConfig(DefaultsConfig): 173 """ 174 UserConfig class, based on ConfigParser 175 name: name of the config 176 defaults: dictionnary containing options 177 *or* list of tuples (section_name, options) 178 version: version of the configuration file (X.Y.Z format) 179 subfolder: configuration file will be saved in %home%/subfolder/%name%.ini 180 181 Note that 'get' and 'set' arguments number and type 182 differ from the overriden methods 183 """ 184 DEFAULT_SECTION_NAME = 'main' 185 def __init__(self, name, defaults=None, load=True, version=None, 186 subfolder=None, backup=False, raw_mode=False, 187 remove_obsolete=False): 188 DefaultsConfig.__init__(self, name, subfolder) 189 self.raw = 1 if raw_mode else 0 190 if (version is not None and 191 re.match(r'^(\d+).(\d+).(\d+)$', version) is None): 192 raise ValueError("Version number %r is incorrect - must be in X.Y.Z format" % version) 193 if isinstance(defaults, dict): 194 defaults = [ (self.DEFAULT_SECTION_NAME, defaults) ] 195 self.defaults = defaults 196 if defaults is not None: 197 self.reset_to_defaults(save=False) 198 fname = self.filename() 199 if backup: 200 try: 201 shutil.copyfile(fname, "%s.bak" % fname) 202 except IOError: 203 pass 204 if load: 205 # If config file already exists, it overrides Default options: 206 self.load_from_ini() 207 old_ver = self.get_version(version) 208 _major = lambda _t: _t[:_t.find('.')] 209 _minor = lambda _t: _t[:_t.rfind('.')] 210 # Save new defaults 211 self._save_new_defaults(defaults, version, subfolder) 212 # Updating defaults only if major/minor version is different 213 if _minor(version) != _minor(old_ver): 214 if backup: 215 try: 216 shutil.copyfile(fname, "%s-%s.bak" % (fname, old_ver)) 217 except IOError: 218 pass 219 if check_version(old_ver, '2.4.0', '<'): 220 self.reset_to_defaults(save=False) 221 else: 222 self._update_defaults(defaults, old_ver) 223 # Remove deprecated options if major version has changed 224 if remove_obsolete or _major(version) != _major(old_ver): 225 self._remove_deprecated_options(old_ver) 226 # Set new version number 227 self.set_version(version, save=False) 228 if defaults is None: 229 # If no defaults are defined, set .ini file settings as default 230 self.set_as_defaults() 231 232 def get_version(self, version='0.0.0'): 233 """Return configuration (not application!) version""" 234 return self.get(self.DEFAULT_SECTION_NAME, 'version', version) 235 236 def set_version(self, version='0.0.0', save=True): 237 """Set configuration (not application!) version""" 238 self.set(self.DEFAULT_SECTION_NAME, 'version', version, save=save) 239 240 def load_from_ini(self): 241 """ 242 Load config from the associated .ini file 243 """ 244 try: 245 if PY2: 246 # Python 2 247 fname = self.filename() 248 if osp.isfile(fname): 249 try: 250 with codecs.open(fname, encoding='utf-8') as configfile: 251 self.readfp(configfile) 252 except IOError: 253 print("Failed reading file", fname) # spyder: test-skip 254 else: 255 # Python 3 256 self.read(self.filename(), encoding='utf-8') 257 except cp.MissingSectionHeaderError: 258 print("Warning: File contains no section headers.") # spyder: test-skip 259 260 def _load_old_defaults(self, old_version): 261 """Read old defaults""" 262 old_defaults = cp.ConfigParser() 263 if check_version(old_version, '3.0.0', '<='): 264 path = get_module_source_path('spyder') 265 else: 266 path = osp.dirname(self.filename()) 267 path = osp.join(path, 'defaults') 268 old_defaults.read(osp.join(path, 'defaults-'+old_version+'.ini')) 269 return old_defaults 270 271 def _save_new_defaults(self, defaults, new_version, subfolder): 272 """Save new defaults""" 273 new_defaults = DefaultsConfig(name='defaults-'+new_version, 274 subfolder=subfolder) 275 if not osp.isfile(new_defaults.filename()): 276 new_defaults.set_defaults(defaults) 277 new_defaults._save() 278 279 def _update_defaults(self, defaults, old_version, verbose=False): 280 """Update defaults after a change in version""" 281 old_defaults = self._load_old_defaults(old_version) 282 for section, options in defaults: 283 for option in options: 284 new_value = options[ option ] 285 try: 286 old_value = old_defaults.get(section, option) 287 except (cp.NoSectionError, cp.NoOptionError): 288 old_value = None 289 if old_value is None or \ 290 to_text_string(new_value) != old_value: 291 self._set(section, option, new_value, verbose) 292 293 def _remove_deprecated_options(self, old_version): 294 """ 295 Remove options which are present in the .ini file but not in defaults 296 """ 297 old_defaults = self._load_old_defaults(old_version) 298 for section in old_defaults.sections(): 299 for option, _ in old_defaults.items(section, raw=self.raw): 300 if self.get_default(section, option) is NoDefault: 301 try: 302 self.remove_option(section, option) 303 if len(self.items(section, raw=self.raw)) == 0: 304 self.remove_section(section) 305 except cp.NoSectionError: 306 self.remove_section(section) 307 308 def cleanup(self): 309 """ 310 Remove .ini file associated to config 311 """ 312 os.remove(self.filename()) 313 314 def set_as_defaults(self): 315 """ 316 Set defaults from the current config 317 """ 318 self.defaults = [] 319 for section in self.sections(): 320 secdict = {} 321 for option, value in self.items(section, raw=self.raw): 322 secdict[option] = value 323 self.defaults.append( (section, secdict) ) 324 325 def reset_to_defaults(self, save=True, verbose=False, section=None): 326 """ 327 Reset config to Default values 328 """ 329 for sec, options in self.defaults: 330 if section == None or section == sec: 331 for option in options: 332 value = options[ option ] 333 self._set(sec, option, value, verbose) 334 if save: 335 self._save() 336 337 def _check_section_option(self, section, option): 338 """ 339 Private method to check section and option types 340 """ 341 if section is None: 342 section = self.DEFAULT_SECTION_NAME 343 elif not is_text_string(section): 344 raise RuntimeError("Argument 'section' must be a string") 345 if not is_text_string(option): 346 raise RuntimeError("Argument 'option' must be a string") 347 return section 348 349 def get_default(self, section, option): 350 """ 351 Get Default value for a given (section, option) 352 -> useful for type checking in 'get' method 353 """ 354 section = self._check_section_option(section, option) 355 for sec, options in self.defaults: 356 if sec == section: 357 if option in options: 358 return options[ option ] 359 else: 360 return NoDefault 361 362 def get(self, section, option, default=NoDefault): 363 """ 364 Get an option 365 section=None: attribute a default section name 366 default: default value (if not specified, an exception 367 will be raised if option doesn't exist) 368 """ 369 section = self._check_section_option(section, option) 370 371 if not self.has_section(section): 372 if default is NoDefault: 373 raise cp.NoSectionError(section) 374 else: 375 self.add_section(section) 376 377 if not self.has_option(section, option): 378 if default is NoDefault: 379 raise cp.NoOptionError(option, section) 380 else: 381 self.set(section, option, default) 382 return default 383 384 value = cp.ConfigParser.get(self, section, option, raw=self.raw) 385 # Use type of default_value to parse value correctly 386 default_value = self.get_default(section, option) 387 if isinstance(default_value, bool): 388 value = ast.literal_eval(value) 389 elif isinstance(default_value, float): 390 value = float(value) 391 elif isinstance(default_value, int): 392 value = int(value) 393 elif is_text_string(default_value): 394 if PY2: 395 try: 396 value = value.decode('utf-8') 397 try: 398 # Some str config values expect to be eval after decoding 399 new_value = ast.literal_eval(value) 400 if is_text_string(new_value): 401 value = new_value 402 except (SyntaxError, ValueError): 403 pass 404 except (UnicodeEncodeError, UnicodeDecodeError): 405 pass 406 else: 407 try: 408 # lists, tuples, ... 409 value = ast.literal_eval(value) 410 except (SyntaxError, ValueError): 411 pass 412 return value 413 414 def set_default(self, section, option, default_value): 415 """ 416 Set Default value for a given (section, option) 417 -> called when a new (section, option) is set and no default exists 418 """ 419 section = self._check_section_option(section, option) 420 for sec, options in self.defaults: 421 if sec == section: 422 options[ option ] = default_value 423 424 def set(self, section, option, value, verbose=False, save=True): 425 """ 426 Set an option 427 section=None: attribute a default section name 428 """ 429 section = self._check_section_option(section, option) 430 default_value = self.get_default(section, option) 431 if default_value is NoDefault: 432 # This let us save correctly string value options with 433 # no config default that contain non-ascii chars in 434 # Python 2 435 if PY2 and is_text_string(value): 436 value = repr(value) 437 default_value = value 438 self.set_default(section, option, default_value) 439 if isinstance(default_value, bool): 440 value = bool(value) 441 elif isinstance(default_value, float): 442 value = float(value) 443 elif isinstance(default_value, int): 444 value = int(value) 445 elif not is_text_string(default_value): 446 value = repr(value) 447 self._set(section, option, value, verbose) 448 if save: 449 self._save() 450 451 def remove_section(self, section): 452 cp.ConfigParser.remove_section(self, section) 453 self._save() 454 455 def remove_option(self, section, option): 456 cp.ConfigParser.remove_option(self, section, option) 457 self._save() 458