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