Repository: spyder-ide/spyder
Pull request: PR: Update and modernize config system to support multiple types of configurations
<!--- Make sure to read the Contributing Guidelines:                   --->
<!--- https://github.com/spyder-ide/spyder/blob/master/CONTRIBUTING.md --->
<!--- and follow PEP 8, PEP 257 and Spyder's code style:               --->
<!--- https://github.com/spyder-ide/spyder/wiki/Dev:-Coding-Style      --->

## Description of Changes


* [x] Wrote at least one-line docstrings (for any new functions)
* [x] Added unit test(s) covering the changes (if testable)


<!--- Explain what you've done and why --->




### Issue(s) Resolved

<!--- List the issue(s) below, in the form "Fixes #1234"; one per line --->

Fixes #9802
Fixes #9801

### Affirmation

By submitting this Pull Request or typing my (user)name below,
I affirm the [Developer Certificate of Origin](https://developercertificate.org)
with respect to all commits and content included in this PR,
and understand I am releasing the same under Spyder's MIT (Expat) license.

<!--- TYPE YOUR USER/NAME AFTER THE FOLLOWING: --->
I certify the above statement is true and correct: @goanpeca 

<!--- Thanks for your help making Spyder better for everyone! --->


Problem statement:
Add a configuration manager to handle global user preferences and projects preferences
This configuration manager is a class that will be a direct replacement of the usual CONF. But this manager will handle which configuration to use depending if it is a global or a project situation.
 

Add capability of splitting the Configuration manager files to use
This will allow saving configuration on separate files depending on either the section (a complete section) or an options (or options) per section.

The tricky part is ensuring backward compatibility.
The splitting is not an issue.

Hints from issue discussion:
> The tricky part is ensuring backward compatibility.

I agree. We can't break backwards compatibility at this point.

Commit message: Refactor tests

Patch to review:
diff --git a/spyder/app/mainwindow.py b/spyder/app/mainwindow.py
index 09298a0cd0b..c64409fdc7b 100644
--- a/spyder/app/mainwindow.py
+++ b/spyder/app/mainwindow.py
@@ -95,7 +95,7 @@
 # Proper high DPI scaling is available in Qt >= 5.6.0. This attibute must
 # be set before creating the application.
 #==============================================================================
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 
 if hasattr(Qt, 'AA_EnableHighDpiScaling'):
     QCoreApplication.setAttribute(Qt.AA_EnableHighDpiScaling,
@@ -1435,6 +1435,7 @@ def load_window_settings(self, prefix, default=False, section='main'):
             hexstate = None
         else:
             hexstate = get_func(section, prefix+'state', None)
+
         pos = get_func(section, prefix+'position')
 
         # It's necessary to verify if the window/position value is valid
diff --git a/spyder/app/restart.py b/spyder/app/restart.py
index aaf32bab8d4..30488ed265d 100644
--- a/spyder/app/restart.py
+++ b/spyder/app/restart.py
@@ -30,7 +30,7 @@
 from spyder.config.base import _, get_image_path
 from spyder.py3compat import to_text_string
 from spyder.utils.qthelpers import qapplication
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 
 
 PY2 = sys.version[0] == '2'
diff --git a/spyder/app/start.py b/spyder/app/start.py
index ef7a9f41fc6..bbeac112a9b 100644
--- a/spyder/app/start.py
+++ b/spyder/app/start.py
@@ -31,7 +31,7 @@
 from spyder.app.cli_options import get_options
 from spyder.config.base import (get_conf_path, running_in_mac_app,
                                 running_under_pytest)
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.utils.external import lockfile
 from spyder.py3compat import is_unicode
 
diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py
index bda9c7ad38f..566ef21c352 100644
--- a/spyder/app/tests/test_mainwindow.py
+++ b/spyder/app/tests/test_mainwindow.py
@@ -44,7 +44,7 @@
 from spyder.app import start
 from spyder.app.mainwindow import MainWindow  # Tests fail without this import
 from spyder.config.base import get_home_dir, get_module_path
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.widgets.dock import TabFilter
 from spyder.preferences.runconfig import RunConfiguration
 from spyder.plugins.base import PluginWindow
diff --git a/spyder/config/appearance.py b/spyder/config/appearance.py
index 35b9edfcde7..af8c585d71b 100644
--- a/spyder/config/appearance.py
+++ b/spyder/config/appearance.py
@@ -212,7 +212,7 @@
     'solarized/dark/background':  '#002b36',
     'solarized/dark/currentline': '#083f4d',
     'solarized/dark/currentcell': '#073642',
-    'solarized/dark/occurrence':   '#657b83',
+    'solarized/dark/occurrence':  '#657b83',
     'solarized/dark/ctrlclick':   '#d33682',
     'solarized/dark/sideareas':   '#073642',
     'solarized/dark/matched_p':   '#93a1a1',
@@ -231,7 +231,7 @@
     'inkpot/background':  "#1f1f27",
     'inkpot/currentline': "#2d2d44",
     'inkpot/currentcell': "#33333A",
-    'inkpot/occurence':   "#616161",
+    'inkpot/occurrence':  "#616161",
     'inkpot/ctrlclick':   "#1f1f27",
     'inkpot/sideareas':   "#2d2d44",
     'inkpot/matched_p':   "#cfbfad",
@@ -250,7 +250,7 @@
     'minimal/background':  "#ffffff",
     'minimal/currentline': "#aaccff",
     'minimal/currentcell': "#E7F1FF",
-    'minimal/occurence':   "#efefef",
+    'minimal/occurrence':  "#efefef",
     'minimal/ctrlclick':   "#05314d",
     'minimal/sideareas':   "#aaccff",
     'minimal/matched_p':   "#000000",
@@ -269,7 +269,7 @@
     'nightlion/background':  "#1e1e1e",
     'nightlion/currentline': "#505050",
     'nightlion/currentcell': "#323232",
-    'nightlion/occurence':   "#616161",
+    'nightlion/occurrence':  "#616161",
     'nightlion/ctrlclick':   "#b3b5af",
     'nightlion/sideareas':   "#505050",
     'nightlion/matched_p':   "#e2e2e2",
@@ -288,7 +288,7 @@
     'notepad++/background':  "#ffffff",
     'notepad++/currentline': "#eeeeee",
     'notepad++/currentcell': "#D9D9D9",
-    'notepad++/occurence':   "#efefef",
+    'notepad++/occurrence':  "#efefef",
     'notepad++/ctrlclick':   "#800080",
     'notepad++/sideareas':   "#eeeeee",
     'notepad++/matched_p':   "#8000ff",
@@ -307,7 +307,7 @@
     'oblivion/background':  "#1e1e1e",
     'oblivion/currentline': "#2a2a2a",
     'oblivion/currentcell': "#323232",
-    'oblivion/occurence':   "#000000",
+    'oblivion/occurrence':  "#000000",
     'oblivion/ctrlclick':   "#ccdf32",
     'oblivion/sideareas':   "#2a2a2a",
     'oblivion/matched_p':   "#d8d8d8",
@@ -326,7 +326,7 @@
     'obsidian/background':  "#293134",
     'obsidian/currentline': "#2f393c",
     'obsidian/currentcell': "#3C4346",
-    'obsidian/occurence':   "#616161",
+    'obsidian/occurrence':  "#616161",
     'obsidian/ctrlclick':   "#7d8c93",
     'obsidian/sideareas':   "#2f393c",
     'obsidian/matched_p':   "#e0e2e4",
@@ -345,7 +345,7 @@
     'pastel/background':  "#1f2223",
     'pastel/currentline': "#2f393c",
     'pastel/currentcell': "#454849",
-    'pastel/occurence':   "#616161",
+    'pastel/occurrence':  "#616161",
     'pastel/ctrlclick':   "#7d8c93",
     'pastel/sideareas':   "#2f393c",
     'pastel/matched_p':   "#e0e2e4",
@@ -364,7 +364,7 @@
     'retta/background':  "#000000",
     'retta/currentline': "#2a2a2a",
     'retta/currentcell': "#171717",
-    'retta/occurence':   "#5e5c56",
+    'retta/occurrence':  "#5e5c56",
     'retta/ctrlclick':   "#83786e",
     'retta/sideareas':   "#2a2a2a",
     'retta/matched_p':   "#f8e1aa",
@@ -383,7 +383,7 @@
     'roboticket/background':  "#f5f5f5",
     'roboticket/currentline': "#e0e0ff",
     'roboticket/currentcell': "#CCCCE8",
-    'roboticket/occurence':   "#ffcfbb",
+    'roboticket/occurrence':  "#ffcfbb",
     'roboticket/ctrlclick':   "#ad95af",
     'roboticket/sideareas':   "#e0e0ff",
     'roboticket/matched_p':   "#585858",
@@ -402,7 +402,7 @@
     'sublime-monokai/extended/background':  "#222222",
     'sublime-monokai/extended/currentline': "#2f2f2f",
     'sublime-monokai/extended/currentcell': "#363636",
-    'sublime-monokai/extended/occurence':   "#000000",
+    'sublime-monokai/extended/occurrence':  "#000000",
     'sublime-monokai/extended/ctrlclick':   "#ffffff",
     'sublime-monokai/extended/sideareas':   "#2f2f2f",
     'sublime-monokai/extended/matched_p':   "#cfbfad",
@@ -421,7 +421,7 @@
     'vibrant-ink/background':  "#191919",
     'vibrant-ink/currentline': "#222220",
     'vibrant-ink/currentcell': "#2D2D2D",
-    'vibrant-ink/occurence':   "#616161",
+    'vibrant-ink/occurrence':  "#616161",
     'vibrant-ink/ctrlclick':   "#8c3fc8",
     'vibrant-ink/sideareas':   "#222220",
     'vibrant-ink/matched_p':   "#ffffff",
diff --git a/spyder/config/base.py b/spyder/config/base.py
index 956f88c5408..284c0c3f295 100644
--- a/spyder/config/base.py
+++ b/spyder/config/base.py
@@ -81,6 +81,7 @@ def use_dev_config_dir(use_dev_config_dir=USE_DEV_CONFIG_DIR):
             use_dev_config_dir = False
     else:
         use_dev_config_dir = DEV or not is_stable_version(__version__)
+
     return use_dev_config_dir
 
 
@@ -116,36 +117,42 @@ def debug_print(*message):
 #==============================================================================
 # Configuration paths
 #==============================================================================
-# Spyder settings dir
-# NOTE: During the 2.x.x series this dir was named .spyder2, but
-# since 3.0+ we've reverted back to use .spyder to simplify major
-# updates in version (required when we change APIs by Linux
-# packagers)
-if sys.platform.startswith('linux'):
-    SUBFOLDER = 'spyder'
-else:
-    SUBFOLDER = '.spyder'
+def get_conf_subfolder():
+    """Return the configuration subfolder for different ooperating systems."""
+    # Spyder settings dir
+    # NOTE: During the 2.x.x series this dir was named .spyder2, but
+    # since 3.0+ we've reverted back to use .spyder to simplify major
+    # updates in version (required when we change APIs by Linux
+    # packagers)
+    if sys.platform.startswith('linux'):
+        SUBFOLDER = 'spyder'
+    else:
+        SUBFOLDER = '.spyder'
 
+    # We can't have PY2 and PY3 settings in the same dir because:
+    # 1. This leads to ugly crashes and freezes (e.g. by trying to
+    #    embed a PY2 interpreter in PY3)
+    # 2. We need to save the list of installed modules (for code
+    #    completion) separately for each version
+    if PY3:
+        SUBFOLDER = SUBFOLDER + '-py3'
 
-# We can't have PY2 and PY3 settings in the same dir because:
-# 1. This leads to ugly crashes and freezes (e.g. by trying to
-#    embed a PY2 interpreter in PY3)
-# 2. We need to save the list of installed modules (for code
-#    completion) separately for each version
-if PY3:
-    SUBFOLDER = SUBFOLDER + '-py3'
+    # If running a development/beta version, save config in a separate
+    # directory to avoid wiping or contaiminating the user's saved stable
+    # configuration.
+    if use_dev_config_dir():
+        SUBFOLDER = SUBFOLDER + '-dev'
+
+    return SUBFOLDER
 
 
-# If running a development/beta version, save config in a seperate directory
-# to avoid wiping or contaiminating the user's saved stable configuration.
-if use_dev_config_dir():
-    SUBFOLDER = SUBFOLDER + '-dev'
+def get_project_config_folder():
+    """Return the default project configuration folder."""
+    return '.spyproject'
 
 
 def get_home_dir():
-    """
-    Return user home directory
-    """
+    """Return user home directory."""
     try:
         # expanduser() returns a raw byte string which needs to be
         # decoded with the codec that the OS is using to represent
@@ -185,7 +192,7 @@ def get_clean_conf_dir():
 
     conf_dir = osp.join(str(tempfile.gettempdir()),
                         'pytest-spyder{0!s}'.format(current_user),
-                        SUBFOLDER)
+                        get_conf_subfolder())
     return conf_dir
 
 
@@ -201,11 +208,13 @@ def get_conf_path(filename=None):
         xdg_config_home = os.environ.get('XDG_CONFIG_HOME', '')
         if not xdg_config_home:
             xdg_config_home = osp.join(get_home_dir(), '.config')
+
         if not osp.isdir(xdg_config_home):
             os.makedirs(xdg_config_home)
-        conf_dir = osp.join(xdg_config_home, SUBFOLDER)
+
+        conf_dir = osp.join(xdg_config_home, get_conf_subfolder())
     else:
-        conf_dir = osp.join(get_home_dir(), SUBFOLDER)
+        conf_dir = osp.join(get_home_dir(), get_conf_subfolder())
 
     # Create conf_dir
     if not osp.isdir(conf_dir):
@@ -213,6 +222,7 @@ def get_conf_path(filename=None):
             os.makedirs(conf_dir)
         else:
             os.mkdir(conf_dir)
+
     if filename is None:
         return conf_dir
     else:
diff --git a/spyder/config/gui.py b/spyder/config/gui.py
index 41a447a18bd..fc62dd5d607 100644
--- a/spyder/config/gui.py
+++ b/spyder/config/gui.py
@@ -23,7 +23,7 @@
 from qtpy.QtWidgets import QShortcut
 
 # Local imports
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.config.user import NoDefault
 from spyder.py3compat import to_text_string
 from spyder.utils import syntaxhighlighters as sh
diff --git a/spyder/config/main.py b/spyder/config/main.py
index ae6e567a24f..434fafed9b6 100755
--- a/spyder/config/main.py
+++ b/spyder/config/main.py
@@ -1,14 +1,14 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright © Spyder Project Contributors
+# # Copyright © Spyder Project Contributors
 # Licensed under the terms of the MIT License
 # (see spyder/__init__.py for details)
 
 """
-Spyder configuration options
+Spyder configuration options.
 
 Note: Leave this file free of Qt related imports, so that it can be used to
-quickly load a user config file
+quickly load a user config file.
 """
 
 import os
@@ -16,8 +16,8 @@
 import sys
 
 # Local import
-from spyder.config.base import (CHECK_ALL, EXCLUDED_NAMES, get_home_dir,
-                                SUBFOLDER)
+from spyder.config.base import (CHECK_ALL, EXCLUDED_NAMES, get_conf_path,
+                                get_home_dir)
 from spyder.config.fonts import MEDIUM, SANS_SERIF
 from spyder.config.user import UserConfig
 from spyder.config.utils import IMPORT_EXT
@@ -287,7 +287,7 @@
               'search_text': [''],
               'search_text_samples': [TASKS_PATTERN],
               'more_options': True,
-              'case_sensitive': False
+              'case_sensitive': False,
               }),
             ('breakpoints',
              {
@@ -495,10 +495,91 @@
               'advanced/port': 2087,
               'advanced/external': False,
               'advanced/stdio': False
-             })
+             }),
             ]
 
 
+NAME_MAP = {
+    # Empty container object means use the rest of defaults
+    'spyder': [],
+    # Splitting these files makes sense for projects, we might as well
+    # apply the same split for the app global config
+    # These options change on spyder startup or are tied to a specific OS,
+    # not good for version control
+    # 'transient': [
+    #     ('main', [
+    #         'completion/size',
+    #         'crash',
+    #         'current_version',
+    #         'historylog_filename',
+    #         'last_visible_toolbars',
+    #         'spyder_pythonpath',
+    #         'window/position',
+    #         'window/prefs_dialog_size',
+    #         'window/size',
+    #         'window/state',
+    #         ]
+    #      ),
+    #     ('appearance', [
+    #         'windows_style',
+    #         ]
+    #      ),
+    #     ('editor', [
+    #         'autosave_mapping',
+    #         'bookmarks',
+    #         'filenames',
+    #         'layout_settings',
+    #         'recent_files',
+    #         'splitter_state',
+    #         ]
+    #      ),
+    #     ('explorer', [
+    #         'file_associations',
+    #     ]),
+    #     ('find_in_files', [
+    #         'path_history'
+    #         'search_text',
+    #         ]
+    #      ),
+    #     ('main_interpreter', [
+    #         'custom_interpreters_list',
+    #         'custom_interpreter',
+    #         'executable',
+    #          ]
+    #      ),
+    #     ('onlinehelp', [
+    #         'zoom_factor',
+    #          ]
+    #      ),
+    #     ('outline_explorer', [
+    #         'expanded_state',
+    #         'scrollbar_position',
+    #         ],
+    #      ),
+    #     ('project_explorer', [
+    #         'current_project_path',
+    #         'expanded_state',
+    #         'recent_projects',
+    #         'scrollbar_position',
+    #       ]
+    #      ),
+    #     ('quick_layouts', []), # Empty list means use all options
+    #     ('run', [
+    #         'breakpoints',
+    #         'configurations',
+    #         'defaultconfiguration',
+    #         'default/wdir/fixed_directory',
+    #       ]
+    #      ),
+    #     ('workingdir', [
+    #         'console/fixed_directory',
+    #         'startup/fixed_directory',
+    #       ]
+    #      ),
+    # ]
+}
+
+
 # =============================================================================
 # Config instance
 # =============================================================================
@@ -509,20 +590,4 @@
 #    or if you want to *rename* options, then you need to do a MAJOR update in
 #    version, e.g. from 3.0.0 to 4.0.0
 # 3. You don't need to touch this value if you're just adding a new option
-CONF_VERSION = '50.2.0'
-
-
-# Main configuration instance
-try:
-    CONF = UserConfig('spyder', defaults=DEFAULTS, load=True,
-                      version=CONF_VERSION, subfolder=SUBFOLDER, backup=True,
-                      raw_mode=True)
-except Exception:
-    CONF = UserConfig('spyder', defaults=DEFAULTS, load=False,
-                      version=CONF_VERSION, subfolder=SUBFOLDER, backup=True,
-                      raw_mode=True)
-
-# Removing old .spyder.ini location:
-old_location = osp.join(get_home_dir(), '.spyder.ini')
-if osp.isfile(old_location):
-    os.remove(old_location)
+CONF_VERSION = '51.0.0'
diff --git a/spyder/config/manager.py b/spyder/config/manager.py
new file mode 100644
index 00000000000..b31855c1196
--- /dev/null
+++ b/spyder/config/manager.py
@@ -0,0 +1,168 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+Configuration manager providing access to user/site/project configuration.
+"""
+
+# Standard library imports
+import os
+import os.path as osp
+
+# Local imports
+from spyder.config.base import get_conf_path, get_conf_subfolder, get_home_dir
+from spyder.config.main import CONF_VERSION, DEFAULTS, NAME_MAP
+from spyder.config.user import MultiUserConfig, NoDefault
+
+
+class ConfigurationManager(object):
+    """
+    Configuration manager to provide access to user/site/project config.
+    """
+
+    def __init__(self, parent=None, active_project_callback=None):
+        """
+        Configuration manager to provide access to user/site/project config.
+        """
+        path = self.get_user_config_path()
+        if not osp.isdir(path):
+            os.makedirs(path)
+
+        self._parent = parent
+        self._active_project_callback = active_project_callback
+        self._user_config = MultiUserConfig(
+            NAME_MAP,
+            path=path,
+            defaults=DEFAULTS,
+            load=True,
+            version=CONF_VERSION,
+            backup=True,
+            raw_mode=True,
+            remove_obsolete=False,
+        )
+        # TODO: Implementation to be defined
+        self._site_config = None
+
+        # TODO: To be implemented in the following PR
+        self._project_configs = {}  # Cache project configurations
+
+        # Setup
+        self.remove_deprecated_config_locations()
+
+    def remove_deprecated_config_locations(self):
+        """Removing old .spyder.ini location."""
+        old_location = osp.join(get_home_dir(), '.spyder.ini')
+        if osp.isfile(old_location):
+            os.remove(old_location)
+
+    def get_system_config_path(self):
+        """Return the system configuration path."""
+        return None
+
+    def get_active_conf(self):
+        """
+        Return the active project configuration or the user configurarion.
+        """
+        # TODO: implement project configuration on the following PR
+        return self._user_config
+
+    def get_user_config_path(self):
+        """Return the user configuration path."""
+        base_path = get_conf_path()
+        path = osp.join(base_path, 'app', 'config')
+        if not osp.isdir(path):
+            os.makedirs(path)
+
+        return path
+
+    # --- Projects
+    # ------------------------------------------------------------------------
+    def register_config(self, root_path, config):
+        """
+        Register confgiuration with `root_path`.
+
+        Useful for registering project configurations as they are opened.
+        """
+        if self.is_project_root(root_path):
+            if root_path not in self._project_configs:
+                self._project_configs[root_path] = config
+        else:
+            # Validate which are valid site config locations
+            self._site_config = config
+
+    def get_active_project(self):
+        """Return the `root_path` of the current active project."""
+        callback = self._active_project_callback
+        if self._active_project_callback:
+            return callback()
+
+    def is_project_root(self, root_path):
+        """Check if `root_path` corresponds to a valid spyder project."""
+        return False
+
+    def get_project_config_path(self, project_root):
+        """Return the project configuration path."""
+        path = osp.join(project_root, '.spyproj', 'config')
+        if not osp.isdir(path):
+            os.makedirs(path)
+
+    # MultiUserConf/UserConf interface
+    # ------------------------------------------------------------------------
+    def items(self, section):
+        """Return all the items option/values for the given section."""
+        config = self.get_active_conf()
+        return config.items(section)
+
+    def options(self, section):
+        """Return all the options for the given section."""
+        config = self.get_active_conf()
+        return config.options(section)
+
+    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_active_conf()
+        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_active_conf()
+        config.set(section=section, option=option, value=value,
+                   verbose=verbose, save=save)
+
+    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_active_conf()
+        return config.get_default(section, option)
+
+    def remove_section(self, section):
+        """Remove `section` and all options within it."""
+        config = self.get_active_conf()
+        config.remove_section(section)
+
+    def remove_option(self, section, option):
+        """Remove `option` from `section`."""
+        config = self.get_active_conf()
+        config.remove_option(section, option)
+
+    def reset_to_defaults(self):
+        """Reset config to Default values."""
+        config = self.get_active_conf()
+        config.reset_to_defaults()
+
+
+CONF = ConfigurationManager()
diff --git a/spyder/config/tests/conftest.py b/spyder/config/tests/conftest.py
new file mode 100644
index 00000000000..cd7d6a80cd2
--- /dev/null
+++ b/spyder/config/tests/conftest.py
@@ -0,0 +1,176 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2009- Spyder Project Contributors
+# Distributed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""Tests for the `spyder.config`."""
+
+# Standard library imports
+import os
+
+# Third party imports
+import pytest
+
+# Local imports
+from spyder.config.base import get_conf_path
+from spyder.config.user import DefaultsConfig, SpyderUserConfig, UserConfig
+from spyder.py3compat import PY2
+
+
+@pytest.fixture
+def defaultconfig(tmpdir, request):
+    name = 'defaults-test'
+    path = str(tmpdir)
+    default_kwargs = {'name': name, 'path': path}
+
+    param = getattr(request, 'param', None)
+    if param:
+        modified_kwargs = request.param[0]
+        kwargs = default_kwargs.copy().update(modified_kwargs)
+    else:
+        kwargs = default_kwargs
+
+    return DefaultsConfig(**kwargs)
+
+
+@pytest.fixture
+def userconfig(tmpdir, request):
+    ini_contents = '[main]\nversion = 1.0.0\n\n'
+    if PY2:
+        # strings are quoted in Python2 but not in Python3
+        ini_contents += "[section]\noption = 'value'\n\n"
+    else:
+        ini_contents += "[section]\noption = value\n\n"
+
+    name = 'spyder-test'
+    path = str(tmpdir)
+    default_kwargs = {
+        'name': name,
+        'path': path,
+        'defaults': {},
+        'load': True,
+        'version': '1.0.0',
+        'backup': False,
+        'raw_mode': True,
+        'remove_obsolete': False,
+    }
+
+    param = getattr(request, 'param', None)
+    if param:
+        modified_kwargs = request.param[0]
+        kwargs = default_kwargs.copy().update(modified_kwargs)
+    else:
+        kwargs = default_kwargs
+
+    inifile = tmpdir.join('{}.ini'.format(name))
+    inifile.write(ini_contents)
+
+    return UserConfig(**kwargs)
+
+
+@pytest.fixture
+def spyderconfig(tmpdir, request):
+    ini_contents = '[main]\nversion = 1.0.0\n\n'
+    if PY2:
+        # strings are quoted in Python2 but not in Python3
+        ini_contents += """[ipython_console]
+startup/run_lines = 'value1,value2'
+
+"""
+    else:
+        ini_contents += """[ipython_console]
+startup/run_lines = value1,value2
+
+"""
+
+    name = 'spyder-test'
+    path = str(tmpdir)
+    default_kwargs = {
+        'name': name,
+        'path': path,
+        'defaults': {},
+        'load': True,
+        'version': '1.0.0',
+        'backup': False,
+        'raw_mode': True,
+        'remove_obsolete': False,
+    }
+
+    param = getattr(request, 'param', None)
+    if param:
+        modified_kwargs = param[0]
+        kwargs = default_kwargs.copy()
+        kwargs.update(modified_kwargs)
+    else:
+        kwargs = default_kwargs
+
+    inifile = tmpdir.join('{}.ini'.format(name))
+    inifile.write(ini_contents)
+
+    return SpyderUserConfig(**kwargs)
+
+
+@pytest.fixture
+def spyderconfig_patches_42(tmpdir):
+    ini_contents = '[main]\nversion = 42.0.0\n\n'
+    if PY2:
+        # Strings are quoted in Python2 but not in Python3
+        ini_contents += ("[ipython_console]\nstartup/run_lines = "
+                         "'value1,value2'")
+    else:
+        ini_contents += '[ipython_console]\nstartup/run_lines = value1,value2'
+
+    name = 'spyder'
+    inifile = tmpdir.join('{}.ini'.format(name))
+    inifile.write(ini_contents + '\n\n')
+
+    return SpyderUserConfig(name=name, path=str(tmpdir), defaults={},
+                            load=True, version='43.0.0', backup=False,
+                            raw_mode=True, remove_obsolete=False)
+
+
+@pytest.fixture
+def spyderconfig_patches_45(tmpdir):
+    ini_contents = '[main]\nversion = 45.0.0\n\n'
+    if PY2:
+        # Strings are quoted in Python2 but not in Python3
+        ini_contents += ("[ipython_console]\nstartup/run_lines = "
+                         "'value1,value2'")
+    else:
+        ini_contents += '[ipython_console]\nstartup/run_lines = value1,value2'
+
+    name = 'spyder'
+    inifile = tmpdir.join('{}.ini'.format(name))
+    inifile.write(ini_contents + '\n\n')
+
+    return SpyderUserConfig(name=name, path=str(tmpdir), defaults={},
+                            load=True, version='46.0.0', backup=False,
+                            raw_mode=True, remove_obsolete=False)
+
+
+@pytest.fixture
+def spyderconfig_previous(tmpdir, mocker):
+    ini_contents = '[main]\nversion = 51.0.0\n\n'
+    if PY2:
+        # Strings are quoted in Python2 but not in Python3
+        ini_contents += "[section]\noption = 'value'"
+    else:
+        ini_contents += "[section]\noption = value"
+
+    name = 'spyder'
+    path = str(tmpdir)
+
+    def temp(*args, **kwargs):
+        return path
+
+    mocker.patch('spyder.config.base', 'get_conf_path')
+    inifile = tmpdir.join('{}.ini'.format(name))
+    os.makedirs(str(tmpdir.join('app', 'config')))
+    inifile2 = tmpdir.join('app', 'config', '{}.ini'.format(name))
+    inifile.write(ini_contents + '\n\n')
+    inifile2.write(ini_contents + '\n\n')
+
+    return SpyderUserConfig(name=name, path=path, defaults={}, load=True,
+                            version='51.0.0', backup=False, raw_mode=True,
+                            remove_obsolete=False)
diff --git a/spyder/config/tests/test_user.py b/spyder/config/tests/test_user.py
index 9586b44a021..80ad9067425 100644
--- a/spyder/config/tests/test_user.py
+++ b/spyder/config/tests/test_user.py
@@ -4,57 +4,404 @@
 # Licensed under the terms of the MIT License
 # (see spyder/__init__.py for details)
 
-"""
-Tests for config/user.py
-"""
+"""Tests for config/user.py."""
+
+# Standard library imports
+import os
 
 # Third party imports
 import pytest
 
 # Local imports
-from spyder.config.user import UserConfig
+from spyder.config.main import CONF_VERSION, DEFAULTS
+from spyder.config.user import NoDefault, UserConfig
 from spyder.py3compat import PY2
+from spyder.py3compat import configparser as cp
+from spyder.utils.fixtures import tmpconfig
+
+
+# --- Default config tests
+# ----------------------------------------------------------------------------
+@pytest.mark.parametrize(
+    "test_input, expected",
+    [
+        (('sec', 'opt', 'val'), '[sec][opt] = val\n'),
+        (('sec', 'opt', 50), '[sec][opt] = 50\n'),
+        (('sec', 'opt', [50]), '[sec][opt] = [50]\n'),
+        (('sec', 'opt', (50, 2)), '[sec][opt] = (50, 2)\n'),
+        (('sec', 'opt', {50}), '[sec][opt] = {}\n'.format(
+            'set([50])' if PY2 else '{50}')),
+        (('sec', 'opt', {'k': 50}), "[sec][opt] = {'k': 50}\n"),
+        (('sec', 'opt', False), '[sec][opt] = False\n'),
+        (('sec', 'opt', True), '[sec][opt] = True\n'),
+        (('sec space', 'opt', True), '[sec space][opt] = True\n'),
+        (('sec space', 'opt space', True), '[sec space][opt space] = True\n'),
+    ]
+)
+def test_default_config_set(defaultconfig, capsys, test_input, expected):
+    section, option, value = test_input
+    defaultconfig._set(section, option, value, verbose=True)
+    assert defaultconfig.sections() == [section]
+
+    captured = capsys.readouterr()
+    assert captured.out == expected
+
+
+def test_default_config_save_write(defaultconfig):
+    defaultconfig._save()
+
+
+def test_default_config_set_defaults(defaultconfig):
+    defaults = [('main2', {'opt': 1})]
+    defaultconfig.set_defaults(defaults)
+    assert 'main2' in defaultconfig.sections()
+
+
+# --- User config tests
+# ============================================================================
+# --- Helpers and checkers
+@pytest.mark.parametrize('value,expected', [
+    ('3.2.1', '3.2'),
+    ('3.2.0', '3.2'),
+])
+def test_userconfig_get_minor_version(value, expected):
+    result = UserConfig._get_minor_version(value)
+    assert result == expected
+
+
+@pytest.mark.parametrize('value,expected', [
+    ('3.2.1', '3'),
+    ('0.2.0', '0'),
+])
+def test_userconfig_get_major_version(value, expected):
+    result = UserConfig._get_major_version(value)
+    assert result == expected
+
+
+@pytest.mark.parametrize('test_version', [
+    ('abc'),
+    ('x.x.x'),
+    ('1.0'),
+    ('-1.0'),
+    ('-1.0.0'),
+    (''),
+])
+def test_userconfig_check_version(tmpdir, test_version, userconfig):
+    name = 'spyder-test'
+    path = str(tmpdir)
+    with pytest.raises(ValueError):
+        UserConfig(name=name, path=path, defaults=DEFAULTS,
+                   load=False, version=test_version, raw_mode=True)
+
+    # Check that setting a wrong version also runs the version checks
+    with pytest.raises(ValueError):
+        userconfig.set_version(test_version)
+
+
+def test_userconfig_check_defaults(tmpdir, capsys):
+    name = 'foobar'
+    path = str(tmpdir)
+    conf = UserConfig(name=name, path=path, defaults={},
+                      load=False, version='1.0.0', backup=False,
+                      raw_mode=True)
+
+    conf._check_defaults({})
+    conf._check_defaults({'option2': 'value2'})
+    conf._check_defaults([])
+    conf._check_defaults([('sec', {'opt1': 'val1'})])
+
+    with pytest.raises(AssertionError):
+        conf._check_defaults([(123, {'opt1': 'val1'})])
+
+    with pytest.raises(AssertionError):
+        conf._check_defaults([('sec', {123: 'val1'})])
+
+    with pytest.raises(ValueError):
+        conf._check_defaults({1, 2, 3})
+
+    with pytest.raises(ValueError):
+        conf._check_defaults('asd')
+
+
+def test_userconfig_check_section_option(userconfig):
+    section = userconfig._check_section_option(None, 'version')
+    assert section == userconfig.DEFAULT_SECTION_NAME
+
+    section = userconfig._check_section_option(None, 'opt')
+    assert section == userconfig.DEFAULT_SECTION_NAME
+
+    section = userconfig._check_section_option('sec', 'opt')
+    assert section == 'sec'
+
+    # Check with invalid section
+    with pytest.raises(RuntimeError):
+        section = userconfig._check_section_option(123, 'opt')
+
+    # Check with invalid option
+    with pytest.raises(RuntimeError):
+        section = userconfig._check_section_option(None, 123)
+
+
+def test_userconfig_load_from_ini(tmpdir, capsys):
+    # Test error on loading
+    name = 'foobar'
+    path = str(tmpdir)
+    fpath = os.path.join(path, '{}.ini'.format(name))
+    with open(fpath, 'w') as fh:
+        fh.write('[sec\n')
+
+    conf = UserConfig(name=name, path=path, defaults=DEFAULTS,
+                      load=True, version=CONF_VERSION, backup=False,
+                      raw_mode=True)
+    assert conf.get_config_fpath() == fpath
+
+    captured = capsys.readouterr()
+    assert 'Warning' in captured.out
+
+
+# --- Public API
+def test_userconfig_get_version(userconfig, tmpconfig):
+    assert tmpconfig.get_version() == CONF_VERSION
+    assert userconfig.get_version() == '1.0.0'
+    userconfig.remove_option('main', 'version')
+    assert userconfig.get_version() == '0.0.0'
 
-@pytest.fixture
-def userconfig(tmpdir, monkeypatch):
-    monkeypatch.setattr('spyder.config.user.get_conf_path', lambda: str(tmpdir))
-    inifile = tmpdir.join('foo.ini')
-    iniContents = '[main]\nversion = 1.0.0\n\n'
-    if PY2: # strings are quoted in Python2 but not in Python3
-        iniContents += "[section]\noption = 'value'\n\n"
-    else:
-        iniContents += "[section]\noption = value\n\n"
-    inifile.write(iniContents)
-    return UserConfig('foo', defaults={}, subfolder=True,
-                      version='1.0.0', raw_mode=True)
 
+def test_userconfig_set_version(userconfig):
+    version = '1000.1000.1000'
+    userconfig.set_version(version)
+    assert userconfig.get_version() == version
 
-def test_userconfig_set_percentage_string(userconfig):
-    """Test to set an option with a '%'."""
-    userconfig.set('section', 'option', '%value')
-    assert userconfig.get('section', 'option') == '%value'
 
+def test_userconfig_reset_to_defaults(tmpdir):
+    name = 'foobar'
+    path = str(tmpdir)
+    defaults = [('main', {'opt': False}), ('test', {'opt': False})]
+    conf = UserConfig(name=name, path=path, defaults=defaults,
+                      load=False, version='1.0.0', backup=False,
+                      raw_mode=True)
+    # Skip section, should go to default
+    assert conf.defaults == defaults
+    conf.set(None, 'opt', True)
+    assert conf.get(None, 'opt') is True
+    conf.reset_to_defaults()
+    assert conf.get(None, 'opt') is False
 
-def test_userconfig_get_string_from_inifile(userconfig):
+    # Provide section, should go to sectio
+    assert conf.defaults == defaults
+    conf.set('test', 'opt', True)
+    assert conf.get('test', 'opt') is True
+    conf.reset_to_defaults()
+    assert conf.get('test', 'opt') is False
+
+
+def test_userconfig_set_as_defaults(tmpdir):
+    name = 'foobar'
+    path = str(tmpdir)
+    conf = UserConfig(name=name, path=path, defaults={},
+                      load=False, version='1.0.0', backup=False,
+                      raw_mode=True)
+
+    # TODO: Is this expected? this seems inconsistent
+    assert conf.defaults == [('main', {})]
+    conf.set_as_defaults()
+    assert conf.defaults == []
+
+
+def test_userconfig_get_default(userconfig, tmpconfig):
+    # Not existing and no defaults
+    value = userconfig.get_default('other_section', 'other_option')
+    assert value == NoDefault
+
+    # Existing and no defaults
+    value = userconfig.get_default('section', 'option')
+    assert value == NoDefault
+
+    # Existing and defaults
+    value = tmpconfig.get_default('main', 'window/is_maximized')
+    assert value is True
+
+
+class TestUserConfigGet:
+
+    @pytest.mark.parametrize(
+        'defaults,value',
+        [
+            # Valid values
+            ([('test', {'opt': 'value'})], 'value'),
+            ([('test', {'opt': u'"éàÇÃãéèï"'})], u'"éàÇÃãéèï"'),
+            ([('test', {'opt': 'éàÇÃãéèï'})], u'éàÇÃãéèï'),
+            ([('test', {'opt': True})], True),
+            ([('test', {'opt': UserConfig})], repr(UserConfig)),
+            ([('test', {'opt': 123})], 123),
+            ([('test', {'opt': 123.123})], 123.123),
+            ([('test', {'opt': [1]})], [1]),
+            ([('test', {'opt': {'key': 'val'}})], {'key': 'val'}),
+        ]
+    )
+    def test_userconfig_get(self, defaults, value, tmpdir):
+        name = 'foobar'
+        path = str(tmpdir)
+        conf = UserConfig(name=name, path=path, defaults=defaults,
+                          load=False, version='1.0.0', backup=False,
+                          raw_mode=True)
+
+        assert conf.get('test', 'opt') == value
+
+    @pytest.mark.parametrize(
+        'defaults,default,raises',
+        [
+            # Valid values
+            ([('test2', {'opt': 'value'})], 'val', True),
+            ([('test2', {'opt': 'value'})], 'val', False),
+            ([('test', {'opt': 'value'})], 'val', False),
+        ]
+    )
+    def test_userconfig_get2(self, defaults, default, raises, tmpdir):
+        name = 'foobar'
+        path = str(tmpdir)
+        conf = UserConfig(name=name, path=path, defaults=defaults,
+                          load=False, version='1.0.0', backup=False,
+                          raw_mode=True)
+
+        if raises:
+            with pytest.raises(cp.NoSectionError):
+                conf.get('test', 'opt')
+        else:
+            conf.get('test', 'opt', default)
+
+    def test_userconfig_get_string_from_inifile(self, userconfig):
+        assert userconfig.get('section', 'option') == 'value'
+
+    def test_userconfig_get_does_not_eval_functions(self, userconfig):
+        # Regression test for spyder-ide/spyder#3354.
+        userconfig.set('section', 'option', 'print("foo")')
+        assert userconfig.get('section', 'option') == 'print("foo")'
+
+
+def test_userconfig_set_default(userconfig):
+    value = userconfig.get_default('section', 'option')
+    assert value == NoDefault
+
+    # TODO: Is this expected behavior? should. If no defaults are provided
+    # then they cannot be set individually until set_as_defaults() is ran?
+    default_value = 'foobar'
+    value = userconfig.set_default('section', 'option', default_value)
+    value = userconfig.get_default('section', 'option')
+    assert value == NoDefault
+
+    userconfig.set_as_defaults()
+    value = userconfig.get_default('section', 'option')
+    expected = "'value'" if PY2 else 'value'
+    assert value == expected
+    value = userconfig.set_default('section', 'option', default_value)
+    value = userconfig.get_default('section', 'option')
+    assert value == default_value
+
+
+class TestUserConfigSet:
+
+    @pytest.mark.parametrize(
+        'defaults,value',
+        [
+            # Valid values
+            ([('test', {'opt': 'value'})], 'other'),
+            ([('test', {'opt': 'éàÇÃãéèï'})], u'ãéèï'),
+            ([('test', {'opt': True})], False),
+            ([('test', {'opt': UserConfig})], dict),
+            ([('test', {'opt': 123})], 345),
+            ([('test', {'opt': 123.123})], 345.345),
+            ([('test', {'opt': [1]})], [1]),
+            ([('test', {'opt': {'key': 'val'}})], {'key2': 'val2'}),
+            # Value not in defaults
+            ([('test', {'opt1': 'value'})], 'other'),
+        ]
+    )
+    def test_userconfig_set_valid(self, defaults, value, tmpdir):
+        name = 'foobar'
+        path = str(tmpdir)
+        conf = UserConfig(name=name, path=path, defaults=defaults,
+                          load=False, version='1.0.0', backup=False,
+                          raw_mode=True)
+        conf.set('test', 'opt', value)
+
+    @pytest.mark.parametrize(
+        'defaults,value',
+        [
+            ([('test', {'opt': 123})], 'no'),
+            ([('test', {'opt': 123.123})], 'n9'),
+            ([('test', {'opt': 123.123})], 'n9'),
+        ]
+    )
+    def test_userconfig_set_invalid(self, defaults, value, tmpdir):
+        name = 'foobar'
+        path = str(tmpdir)
+        conf = UserConfig(name=name, path=path, defaults=defaults,
+                         load=False, version='1.0.0', backup=False,
+                          raw_mode=True)
+
+        with pytest.raises(ValueError):
+            conf.set('test', 'opt', value)
+
+    def test_userconfig_set_with_string(self, userconfig):
+        userconfig.set('section', 'option', 'new value')
+        with open(userconfig.get_config_fpath()) as inifile:
+            ini_contents = inifile.read()
+
+        expected = '[main]\nversion = 1.0.0\n\n'
+        if PY2:
+            expected += "[section]\noption = 'new value'\n\n"
+        else:
+            expected += "[section]\noption = new value\n\n"
+
+        assert ini_contents == expected
+
+    def test_userconfig_set_percentage_string(self, userconfig):
+        """Test to set an option with a '%'."""
+        userconfig.set('section', 'option', '%value')
+        assert userconfig.get('section', 'option') == '%value'
+
+
+def test_userconfig_remove_section(userconfig):
+    assert 'section' in userconfig.sections()
+    userconfig.remove_section('section')
+    assert 'section' not in userconfig.sections()
+
+
+def test_userconfig_remove_option(userconfig):
     assert userconfig.get('section', 'option') == 'value'
+    userconfig.remove_option('section', 'option')
+    with pytest.raises(cp.NoOptionError):
+        userconfig.get('section', 'option')
+
+
+def test_userconfig_cleanup(userconfig):
+    configpath = userconfig.get_config_fpath()
+    assert os.path.isfile(configpath)
+    userconfig.cleanup()
+    assert not os.path.isfile(configpath)
 
 
-def test_userconfig_get_does_not_eval_functions(userconfig):
-    # Regression test for spyder-ide/spyder#3354.
-    userconfig.set('section', 'option', 'print("foo")')
-    assert userconfig.get('section', 'option') == 'print("foo")'
+# --- SpyderUserConfig tests
+# ============================================================================
+# --- Compatibility API
+class TestSpyderConfigApplyPatches:
 
+    def test_spyderconfig_apply_configuration_patches_42(
+            self, spyderconfig_patches_42):
+        # Check that the value is updated
+        value = spyderconfig_patches_42.get('ipython_console',
+                                            'startup/run_lines')
+        expected_value = 'value1; value2'
+        assert value == expected_value
 
-def test_userconfig_set_with_string(userconfig):
-    userconfig.set('section', 'option', 'new value')
-    with open(userconfig.filename()) as inifile:
-        iniContents = inifile.read()
-    expected = '[main]\nversion = 1.0.0\n\n'
-    if PY2:
-        expected += "[section]\noption = 'new value'\n\n"
-    else:
-        expected += "[section]\noption = new value\n\n"
-    assert iniContents == expected
+    def test_spyderconfig_apply_configuration_patches_45(
+            self, spyderconfig_patches_45):
+        # Check that the value is not updated
+        value = spyderconfig_patches_45.get('ipython_console',
+                                            'startup/run_lines')
+        expected_value = 'value1,value2'
+        assert value == expected_value
 
 
 if __name__ == "__main__":
diff --git a/spyder/config/user.py b/spyder/config/user.py
index a825b081fb6..b601625723b 100644
--- a/spyder/config/user.py
+++ b/spyder/config/user.py
@@ -5,15 +5,17 @@
 # (see spyder/__init__.py for details)
 
 """
-This module provides user configuration file management features for Spyder
+This module provides user configuration file management features for Spyder.
 
-It's based on the ConfigParser module (present in the standard library).
+It is based on the ConfigParser module present in the standard library.
 """
 
-from __future__ import print_function
+from __future__ import print_function, unicode_literals
 
-# Std imports
+# Standard library imports
 import ast
+import copy
+import io
 import os
 import os.path as osp
 import re
@@ -21,353 +23,480 @@
 import time
 
 # Local imports
-from spyder.config.base import (get_conf_path, get_home_dir,
-                                get_module_source_path)
-from spyder.utils.programs import check_version
+from spyder.config.base import get_conf_path, get_module_source_path
 from spyder.py3compat import configparser as cp
-from spyder.py3compat import PY2, is_text_string, to_text_string
-
-# Std imports for Python 2
-if PY2:
-    import codecs
+from spyder.py3compat import is_text_string, PY2, to_text_string
+from spyder.utils.programs import check_version
 
 
-#==============================================================================
+# ============================================================================
 # Auxiliary classes
-#==============================================================================
+# ============================================================================
 class NoDefault:
     pass
 
 
-#==============================================================================
+# ============================================================================
 # Defaults class
-#==============================================================================
-class DefaultsConfig(cp.ConfigParser):
+# ============================================================================
+class DefaultsConfig(cp.ConfigParser, object):
     """
-    Class used to save defaults to a file and as base class for
-    UserConfig
+    Class used to save defaults to a file and as UserConfig base class.
     """
-    def __init__(self, name, subfolder):
+
+    def __init__(self, name, path):
+        """
+        Class used to save defaults to a file and as UserConfig base class.
+        """
         if PY2:
-            cp.ConfigParser.__init__(self)
+            super(DefaultsConfig, self).__init__()
         else:
-            cp.ConfigParser.__init__(self, interpolation=None)
-        self.name = name
-        self.subfolder = subfolder
+            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):
         """
-        Private write method for Python 2
-        The one from configparser fails for non-ascii Windows accounts
+        Write method for Python 2.
+
+        The one from configparser fails for non-ascii Windows accounts.
         """
         if self._defaults:
-            fp.write("[%s]\n" % cp.DEFAULTSECT)
+            fp.write('[{}]\n'.format(cp.DEFAULTSECT))
             for (key, value) in self._defaults.items():
-                fp.write("%s = %s\n" % (key, str(value).replace('\n', '\n\t')))
-            fp.write("\n")
+                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("[%s]\n" % section)
+            fp.write('[{}]\n'.format(section))
             for (key, value) in self._sections[section].items():
-                if key == "__name__":
+                if key == '__name__':
                     continue
+
                 if (value is not None) or (self._optcre == self.OPTCRE):
                     value = to_text_string(value)
-                    key = " = ".join((key, value.replace('\n', '\n\t')))
-                fp.write("%s\n" % (key))
-            fp.write("\n")
+                    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):
-        """
-        Private set method
-        """
+        """Set method."""
         if not self.has_section(section):
-            self.add_section( section )
+            self.add_section(section)
+
         if not is_text_string(value):
-            value = repr( value )
+            value = repr(value)
+
         if verbose:
-            print('%s[ %s ] = %s' % (section, option, value))  # spyder: test-skip
-        cp.ConfigParser.set(self, section, option, value)
+            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
-        """
-        # See spyder-ide/spyder#1086 and spyder-ide/spyder#1242 for background
-        # on why this method contains all the exception handling.
-        fname = self.filename()
+        """Save config into the associated .ini file."""
+        fpath = self.get_config_fpath()
 
-        def _write_file(fname):
-            if PY2:
-                # Python 2
-                with codecs.open(fname, 'w', encoding='utf-8') as configfile:
+        def _write_file(fpath):
+            with io.open(fpath, 'w', encoding='utf-8') as configfile:
+                if PY2:
                     self._write(configfile)
-            else:
-                # Python 3
-                with open(fname, 'w', encoding='utf-8') as configfile:
+                else:
                     self.write(configfile)
 
-        try:  # the "easy" way
-            _write_file(fname)
+        # 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(fname):
-                    os.remove(fname)
+            try:
+                # The "delete and sleep" way
+                if osp.isfile(fpath):
+                    os.remove(fpath)
+
                 time.sleep(0.05)
-                _write_file(fname)
+                _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('Failed to write user configuration file to disk, with '
+                      'the exception shown below')  # spyder: test-skip
                 print(e)  # spyder: test-skip
 
-    def filename(self):
-        """Defines the name of the configuration file to use."""
-        # Needs to be done this way to be used by the project config.
-        # To fix on a later PR
-        self._filename = getattr(self, '_filename', None)
-        self._root_path = getattr(self, '_root_path', None)
-
-        if self._filename is None and self._root_path is None:
-            return self._filename_global()
-        else:
-            return self._filename_projects()
-
-    def _filename_projects(self):
-        """Create a .ini filename located in the current project directory.
-        This .ini files stores the specific project preferences for each
-        project created with spyder.
-        """
-        return osp.join(self._root_path, self._filename)
-
-    def _filename_global(self):
-        """Create a .ini filename located in user home directory.
-        This .ini files stores the global spyder preferences.
-        """
-        if self.subfolder is None:
-            config_file = osp.join(get_home_dir(), '.%s.ini' % self.name)
-            return config_file
-        else:
-            folder = get_conf_path()
-            # Save defaults in a "defaults" dir of .spyder2 to not pollute it
-            if 'defaults' in self.name:
-                folder = osp.join(folder, 'defaults')
-                if not osp.isdir(folder):
-                    os.mkdir(folder)
-            config_file = osp.join(folder, '%s.ini' % self.name)
-            return config_file
+    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, False)
+                new_value = options[option]
+                self._set(section, option, new_value, verbose=False)
 
 
-#==============================================================================
+# ============================================================================
 # User config class
-#==============================================================================
+# ============================================================================
 class UserConfig(DefaultsConfig):
     """
-    UserConfig class, based on ConfigParser
-    name: name of the config
-    defaults: dictionnary containing options
-              *or* list of tuples (section_name, options)
-    version: version of the configuration file (X.Y.Z format)
-    subfolder: configuration file will be saved in %home%/subfolder/%name%.ini
-
-    Note that 'get' and 'set' arguments number and type
-    differ from the overriden methods
+    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, {}),]
+        dictionnary containing options *or* list of tuples (sec_name, options)
+    load: bool
+        TODO:
+    version: str
+        version of the configuration file in 'major.minor.micro' format.
+    backup: bool
+        TODO:
+    raw_mode: bool
+        TODO:
+    remove_obsolete: bool
+        TODO:
+
+    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, defaults=None, load=True, version=None,
-                 subfolder=None, backup=False, raw_mode=False,
-                 remove_obsolete=False):
-        DefaultsConfig.__init__(self, name, subfolder)
-        self.raw = 1 if raw_mode else 0
-        if (version is not None and
-                re.match(r'^(\d+).(\d+).(\d+)$', version) is None):
-            raise ValueError("Version number %r is incorrect - must be in X.Y.Z format" % version)
-        if isinstance(defaults, dict):
-            defaults = [ (self.DEFAULT_SECTION_NAME, defaults) ]
-        self.defaults = defaults
-        if defaults is not None:
-            self.reset_to_defaults(save=False)
-        fname = self.filename()
-        if backup:
-            try:
-                shutil.copyfile(fname, "%s.bak" % fname)
-            except IOError:
-                pass
+
+    def __init__(self, name, path, defaults=None, load=True, version=None,
+                 backup=False, raw_mode=False, remove_obsolete=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._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 load:
-            # If config file already exists, it overrides Default options:
-            self.load_from_ini()
-            old_ver = self.get_version(version)
-            _major = lambda _t: _t[:_t.find('.')]
-            _minor = lambda _t: _t[:_t.rfind('.')]
+            # 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(defaults, version, subfolder)
+            self._save_new_defaults(defaults)
+
             # Updating defaults only if major/minor version is different
-            if _minor(version) != _minor(old_ver):
+            if (self._get_minor_version(version=version)
+                    != self._get_minor_version(version=old_version)):
+
                 if backup:
-                    try:
-                        shutil.copyfile(fname, "%s-%s.bak" % (fname, old_ver))
-                    except IOError:
-                        pass
-                if check_version(old_ver, '2.4.0', '<'):
-                    self.reset_to_defaults(save=False)
-                else:
-                    self._update_defaults(defaults, old_ver)
-                if check_version(old_ver, '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)
+                    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 or _major(version) != _major(old_ver):
-                    self._remove_deprecated_options(old_ver)
+                # TODO: This might be problematic for the multi file config
+                # This needs to be called from the multiconfig manager
+                # another option should be added on init for this
+                # if (remove_obsolete or self._get_major_version(version)
+                #         != self._get_major_version(old_version)):
+                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
+                # If no defaults are defined set .ini file settings as default
                 self.set_as_defaults()
 
-    def get_version(self, version='0.0.0'):
-        """Return configuration (not application!) version"""
-        return self.get(self.DEFAULT_SECTION_NAME, 'version', version)
+        if backup:
+            self._make_backup()
+
+    # --- 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 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!')
 
-    def set_version(self, version='0.0.0', save=True):
-        """Set configuration (not application!) version"""
-        self.set(self.DEFAULT_SECTION_NAME, 'version', version, save=save)
+        # This attribute is overriding a method from cp.ConfigParser
+        self.defaults = defaults
 
-    def load_from_ini(self):
+        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):
         """
-        Load config from the associated .ini file
+        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:
             if PY2:
                 # Python 2
-                fname = self.filename()
-                if osp.isfile(fname):
+                if osp.isfile(fpath):
                     try:
-                        with codecs.open(fname, encoding='utf-8') as configfile:
+                        with io.open(fpath, encoding='utf-8') as configfile:
                             self.readfp(configfile)
                     except IOError:
-                        print("Failed reading file", fname)  # spyder: test-skip
+                        error_text = "Failed reading file", fpath
+                        print(error_text)  # spyder: test-skip
             else:
                 # Python 3
-                self.read(self.filename(), encoding='utf-8')
+                self.read(fpath, encoding='utf-8')
+
         except cp.MissingSectionHeaderError:
-            print("Warning: File contains no section headers.")  # spyder: test-skip
+            error_text = 'Warning: File contains no section headers.'
+            print(error_text)  # spyder: test-skip
 
     def _load_old_defaults(self, old_version):
-        """Read old defaults"""
+        """Read old defaults."""
         old_defaults = cp.ConfigParser()
-        if check_version(old_version, '3.0.0', '<='):
-            path = get_module_source_path('spyder')
-        else:
-            path = osp.dirname(self.filename())
-        path = osp.join(path, 'defaults')
-        old_defaults.read(osp.join(path, 'defaults-'+old_version+'.ini'))
+        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, new_version, subfolder):
-        """Save new defaults"""
-        new_defaults = DefaultsConfig(name='defaults-'+new_version,
-                                      subfolder=subfolder)
-        if not osp.isfile(new_defaults.filename()):
+    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"""
+        """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 ]
+                new_value = options[option]
                 try:
-                    old_value = old_defaults.get(section, option)
+                    old_val = old_defaults.get(section, option)
                 except (cp.NoSectionError, cp.NoOptionError):
-                    old_value = None
-                if old_value is None or \
-                  to_text_string(new_value) != old_value:
+                    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
+        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):
+            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:
+                        if len(self.items(section, raw=self._raw)) == 0:
                             self.remove_section(section)
                     except cp.NoSectionError:
                         self.remove_section(section)
 
-    def cleanup(self):
+    # --- 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):
         """
-        Remove .ini file associated to config
+        Return the configuration path for given version.
+
+        If no version is provided, it returns the current file path.
         """
-        os.remove(self.filename())
+        return self.get_config_fpath()
 
-    def set_as_defaults(self):
+    def get_backup_fpath_from_version(self, version=None, old_version=None):
         """
-        Set defaults from the current config
+        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 overriden if versions changed backup location.
         """
-        self.defaults = []
-        for section in self.sections():
-            secdict = {}
-            for option, value in self.items(section, raw=self.raw):
-                secdict[option] = value
-            self.defaults.append( (section, secdict) )
+        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 reset_to_defaults(self, save=True, verbose=False, section=None):
+    def get_defaults_path_name_from_version(self, old_version=None):
+        """
+        Get defaults location based on version.
+
+        To be overriden if versions changed defaults location.
+        """
+        defaults_path = osp.join(osp.dirname(self.get_config_fpath()),
+                                 self._defaults_folder)
+        name = '{}-{}-{}'.format(
+            self._defaults_name_prefix,
+            self._name,
+            self._version,
+        )
+        if not osp.isdir(defaults_path):
+            os.makedirs(defaults_path)
+
+        return defaults_path, name
+
+    def apply_configuration_patches(self, old_version=None):
         """
-        Reset config to Default values
+        Apply any patch to configuration values on version changes.
+
+        To be overriden 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 ]
+                    value = options[option]
                     self._set(sec, option, value, verbose)
         if save:
             self._save()
 
-    def _check_section_option(self, section, option):
-        """
-        Private method to check section and option types
-        """
-        if section is None:
-            section = self.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 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):
+                secdict[option] = value
+            self.defaults.append((section, secdict))
 
     def get_default(self, section, option):
         """
-        Get Default value for a given (section, option)
-        -> useful for type checking in 'get' method
+        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:
-                    return options[ option ]
+                    value = options[option]
+                    break
         else:
-            return NoDefault
+            value = NoDefault
+
+        return value
 
     def get(self, section, option, default=NoDefault):
         """
-        Get an option
-        section=None: attribute a default section name
-        default: default value (if not specified, an exception
-        will be raised if option doesn't exist)
+        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)
 
@@ -384,8 +513,8 @@ def get(self, section, option, default=NoDefault):
                 self.set(section, option, default)
                 return default
 
-        value = cp.ConfigParser.get(self, section, option, raw=self.raw)
-        # Use type of default_value to parse value correctly
+        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)
@@ -398,7 +527,8 @@ def get(self, section, option, default=NoDefault):
                 try:
                     value = value.decode('utf-8')
                     try:
-                        # Some str config values expect to be eval after decoding
+                        # Some str config values expect to be eval after
+                        # decoding
                         new_value = ast.literal_eval(value)
                         if is_text_string(new_value):
                             value = new_value
@@ -408,26 +538,31 @@ def get(self, section, option, default=NoDefault):
                     pass
         else:
             try:
-                # lists, tuples, ...
+                # 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)
-        -> called when a new (section, option) is set and no default exists
+        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
+                options[option] = default_value
 
     def set(self, section, option, value, verbose=False, save=True):
         """
-        Set an option
-        section=None: attribute a default section name
+        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)
@@ -437,8 +572,10 @@ def set(self, section, option, value, verbose=False, save=True):
             # Python 2
             if PY2 and is_text_string(value):
                 value = repr(value)
+
             default_value = value
             self.set_default(section, option, default_value)
+
         if isinstance(default_value, bool):
             value = bool(value)
         elif isinstance(default_value, float):
@@ -447,14 +584,376 @@ def set(self, section, option, value, verbose=False, save=True):
             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):
-        cp.ConfigParser.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):
-        cp.ConfigParser.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())
+
+
+class SpyderUserConfig(UserConfig):
+
+    def get_previous_config_fpath(self):
+        """
+        Override method.
+
+        Return the last configuration file used if found.
+        """
+        fpath = self.get_config_fpath()
+        previous_paths = [
+            # >= 52.0.0
+            fpath,
+            # < 52.0.0
+            os.path.join(get_conf_path(), 'spyder.ini'),
+            # < ??
+        ]
+        for fpath in previous_paths:
+            print('fpath', fpath)
+            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.
+        """
+        fpath = self.get_config_fpath()
+
+        if version is None:
+            fpath = self.get_config_fpath()
+        elif check_version(version, '52.0.0', '<'):
+            fpath = osp.join(get_conf_path(), 'spyder.ini')
+        elif check_version(version, '2.4.0', '<'):
+            # TODO: Should this still be supported?
+            pass
+
+        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, '52.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
+
+    # TODO:
+    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, '3.0.0', '<='):
+                name = '{}-{}'.format('defaults', old_version)
+                path = self._module_source_path
+            elif check_version(old_version, '52.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()
+
+        return path, name
+
+    def apply_configuration_patches(self, old_version=None):
+        """
+        Override method.
+
+        Apply any patch to configuration values on version changes.
+        """
+        # TODO: is this check still needed?
+        if old_version and check_version(old_version, '2.4.0', '<'):
+            self.reset_to_defaults(save=False)
+        else:
+            # TODO: Remove comment if tests pass ok
+            # self._update_defaults(defaults, old_version)
+            self._update_defaults(self.defaults, old_version)
+
+        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):
+    """
+    Multi user config class based emulating the basic interface to UserConfig.
+
+    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):
+        """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._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': remove_obsolete,
+        }
+        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)
+
+    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."""
+        pass
+        # Check it is not repeated
+        # Check filemap names are not repeated or overide default name
+        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 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]
+
+        return config.items(section=section)
+
+    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):
+        """Reset configuration to Default values."""
+        for _, config in self._configs_map.items():
+            config.reset_to_defaults()
+
+    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:
+            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
diff --git a/spyder/plugins/base.py b/spyder/plugins/base.py
index d0900415d06..ef48bc7cf1a 100644
--- a/spyder/plugins/base.py
+++ b/spyder/plugins/base.py
@@ -22,7 +22,7 @@
 # Local imports
 from spyder.config.base import _
 from spyder.config.gui import get_color_scheme, get_font, is_dark_interface
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.config.user import NoDefault
 from spyder.py3compat import configparser, is_text_string
 from spyder.utils import icon_manager as ima
diff --git a/spyder/plugins/breakpoints/widgets/breakpointsgui.py b/spyder/plugins/breakpoints/widgets/breakpointsgui.py
index b2e4fbf7490..336ade96b59 100644
--- a/spyder/plugins/breakpoints/widgets/breakpointsgui.py
+++ b/spyder/plugins/breakpoints/widgets/breakpointsgui.py
@@ -25,7 +25,7 @@
 
 # Local imports
 from spyder.config.base import get_translation
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.utils.qthelpers import (add_actions, create_action,
                                     create_plugin_layout)
 
diff --git a/spyder/plugins/completion/languageserver/confpage.py b/spyder/plugins/completion/languageserver/confpage.py
index c4c8cb4c883..eb67024f6a9 100644
--- a/spyder/plugins/completion/languageserver/confpage.py
+++ b/spyder/plugins/completion/languageserver/confpage.py
@@ -24,7 +24,7 @@
 
 # Local imports
 from spyder.config.base import _
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.config.gui import get_font, is_dark_interface
 from spyder.plugins.completion.languageserver import LSP_LANGUAGES
 from spyder.plugins.editor.widgets.codeeditor import CodeEditor
diff --git a/spyder/plugins/completion/languageserver/plugin.py b/spyder/plugins/completion/languageserver/plugin.py
index 319b4a21a5c..4ec7d5c0a5f 100644
--- a/spyder/plugins/completion/languageserver/plugin.py
+++ b/spyder/plugins/completion/languageserver/plugin.py
@@ -21,7 +21,7 @@
 # Local imports
 from spyder.config.base import get_conf_path, running_under_pytest
 from spyder.config.lsp import PYTHON_CONFIG
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.api.completion import SpyderCompletionPlugin
 from spyder.utils.misc import select_port, getcwd_or_home
 from spyder.plugins.completion.languageserver import LSP_LANGUAGES
diff --git a/spyder/plugins/completion/languageserver/tests/conftest.py b/spyder/plugins/completion/languageserver/tests/conftest.py
index a1ef60c2cf8..f2481a73988 100644
--- a/spyder/plugins/completion/languageserver/tests/conftest.py
+++ b/spyder/plugins/completion/languageserver/tests/conftest.py
@@ -19,7 +19,7 @@
 import pytest
 from pytestqt.plugin import QtBot
 
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.plugins.completion.languageserver import SERVER_CAPABILITES
 from spyder.plugins.completion.languageserver.plugin import (
     LanguageServerPlugin)
diff --git a/spyder/plugins/completion/plugin.py b/spyder/plugins/completion/plugin.py
index 406a2596871..4b31cab92a4 100644
--- a/spyder/plugins/completion/plugin.py
+++ b/spyder/plugins/completion/plugin.py
@@ -20,7 +20,7 @@
 # Local imports
 from spyder.config.base import get_conf_path, running_under_pytest
 from spyder.config.lsp import PYTHON_CONFIG
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.api.completion import SpyderCompletionPlugin
 from spyder.utils.misc import select_port, getcwd_or_home
 from spyder.plugins.completion.languageserver.plugin import (
diff --git a/spyder/plugins/console/plugin.py b/spyder/plugins/console/plugin.py
index 6ac06b1ced4..357d790ebe0 100644
--- a/spyder/plugins/console/plugin.py
+++ b/spyder/plugins/console/plugin.py
@@ -24,7 +24,7 @@
 
 # Local imports
 from spyder.config.base import _, DEV, get_debug_level
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.utils import icon_manager as ima
 from spyder.utils.environ import EnvDialog
 from spyder.utils.misc import (get_error_match, remove_backslashes,
diff --git a/spyder/plugins/console/widgets/internalshell.py b/spyder/plugins/console/widgets/internalshell.py
index b173165d572..18dd59bfb68 100644
--- a/spyder/plugins/console/widgets/internalshell.py
+++ b/spyder/plugins/console/widgets/internalshell.py
@@ -39,7 +39,7 @@
 # In fact, this 'CONF' object has nothing to do in package spyder/widgets
 # which should not contain anything directly related to Spyder's main app
 from spyder.config.base import _, get_conf_path, get_debug_level
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 
 
 builtins.oedit = oedit
diff --git a/spyder/plugins/console/widgets/shell.py b/spyder/plugins/console/widgets/shell.py
index f07b68d969c..84eb0abbb49 100644
--- a/spyder/plugins/console/widgets/shell.py
+++ b/spyder/plugins/console/widgets/shell.py
@@ -29,7 +29,7 @@
 # Local import
 from spyder.config.base import _, get_conf_path, get_debug_level, STDERR
 from spyder.config.gui import config_shortcut, get_shortcut
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.py3compat import (builtins, is_string, is_text_string,
                               PY3, str_lower, to_text_string)
 from spyder.utils import encoding
diff --git a/spyder/plugins/editor/confpage.py b/spyder/plugins/editor/confpage.py
index a0cc610fb26..b80e7c7c147 100644
--- a/spyder/plugins/editor/confpage.py
+++ b/spyder/plugins/editor/confpage.py
@@ -12,7 +12,7 @@
 
 from spyder.api.preferences import PluginConfigPage
 from spyder.config.base import _
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 import spyder.utils.icon_manager as ima
 
 
diff --git a/spyder/plugins/editor/extensions/docstring.py b/spyder/plugins/editor/extensions/docstring.py
index 83c3e43e8a2..eb6580ddaa4 100644
--- a/spyder/plugins/editor/extensions/docstring.py
+++ b/spyder/plugins/editor/extensions/docstring.py
@@ -16,7 +16,7 @@
 from qtpy.QtWidgets import QMenu
 
 # Local imports
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.py3compat import to_text_string
 
 
diff --git a/spyder/plugins/editor/extensions/tests/test_docstring.py b/spyder/plugins/editor/extensions/tests/test_docstring.py
index 11e7999c1e3..fc76da39c7d 100644
--- a/spyder/plugins/editor/extensions/tests/test_docstring.py
+++ b/spyder/plugins/editor/extensions/tests/test_docstring.py
@@ -12,7 +12,7 @@
 from qtpy.QtGui import QTextCursor
 
 # Local imports
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.utils.qthelpers import qapplication
 from spyder.plugins.editor.widgets.codeeditor import CodeEditor
 from spyder.plugins.editor.extensions.docstring import FunctionInfo
diff --git a/spyder/plugins/editor/plugin.py b/spyder/plugins/editor/plugin.py
index a8d443983cf..c71dae2cb0a 100644
--- a/spyder/plugins/editor/plugin.py
+++ b/spyder/plugins/editor/plugin.py
@@ -32,7 +32,7 @@
 from spyder import dependencies
 from spyder.config.base import _, get_conf_path, running_under_pytest
 from spyder.config.gui import get_shortcut
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.config.utils import (get_edit_filetypes, get_edit_filters,
                                  get_filter)
 from spyder.py3compat import PY2, qbytearray_to_str, to_text_string
diff --git a/spyder/plugins/editor/tests/conftest.py b/spyder/plugins/editor/tests/conftest.py
index b9abdd6b266..27532713f8c 100644
--- a/spyder/plugins/editor/tests/conftest.py
+++ b/spyder/plugins/editor/tests/conftest.py
@@ -22,7 +22,7 @@
 from qtpy.QtWidgets import QMainWindow
 import pytest
 
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.plugins.editor.plugin import Editor
 
 
diff --git a/spyder/plugins/editor/utils/autosave.py b/spyder/plugins/editor/utils/autosave.py
index 8e45ef6fe71..cae43abbbe9 100644
--- a/spyder/plugins/editor/utils/autosave.py
+++ b/spyder/plugins/editor/utils/autosave.py
@@ -16,7 +16,7 @@
 
 # Local imports
 from spyder.config.base import _, get_conf_path
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.plugins.editor.widgets.autosaveerror import AutosaveErrorDialog
 from spyder.plugins.editor.widgets.recover import RecoveryDialog
 
diff --git a/spyder/plugins/editor/utils/bookmarks.py b/spyder/plugins/editor/utils/bookmarks.py
index 390526cab02..ae9226e83e8 100644
--- a/spyder/plugins/editor/utils/bookmarks.py
+++ b/spyder/plugins/editor/utils/bookmarks.py
@@ -11,7 +11,7 @@
 import os.path as osp
 
 # Local imports
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 
 
 def _load_all_bookmarks():
diff --git a/spyder/plugins/editor/utils/debugger.py b/spyder/plugins/editor/utils/debugger.py
index f709c266be7..2d943fa2661 100644
--- a/spyder/plugins/editor/utils/debugger.py
+++ b/spyder/plugins/editor/utils/debugger.py
@@ -11,7 +11,7 @@
 
 from qtpy.QtWidgets import QInputDialog, QLineEdit
 
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.config.base import _
 from spyder.py3compat import to_text_string
 from spyder.api.manager import Manager
diff --git a/spyder/plugins/editor/widgets/base.py b/spyder/plugins/editor/widgets/base.py
index 379d994c250..35c4c2dba7a 100644
--- a/spyder/plugins/editor/widgets/base.py
+++ b/spyder/plugins/editor/widgets/base.py
@@ -27,7 +27,7 @@
 
 # Local imports
 from spyder.config.gui import get_font
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.py3compat import PY3, to_text_string
 from spyder.utils import icon_manager as ima
 from spyder.widgets.calltip import CallTipWidget, ToolTipWidget
diff --git a/spyder/plugins/editor/widgets/codeeditor.py b/spyder/plugins/editor/widgets/codeeditor.py
index 47db3ef63bc..6217a94bd76 100644
--- a/spyder/plugins/editor/widgets/codeeditor.py
+++ b/spyder/plugins/editor/widgets/codeeditor.py
@@ -50,7 +50,7 @@
 from spyder.api.panel import Panel
 from spyder.config.base import _, get_debug_level, running_under_pytest
 from spyder.config.gui import get_shortcut, config_shortcut
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.plugins.editor.api.decoration import TextDecoration
 from spyder.plugins.editor.extensions import (CloseBracketsExtension,
                                               CloseQuotesExtension,
diff --git a/spyder/plugins/editor/widgets/editor.py b/spyder/plugins/editor/widgets/editor.py
index 640733bc65b..6b7ae274bb7 100644
--- a/spyder/plugins/editor/widgets/editor.py
+++ b/spyder/plugins/editor/widgets/editor.py
@@ -55,7 +55,7 @@
                                                   ReadWriteStatus, VCSStatus)
 from spyder.plugins.editor.utils.findtasks import find_tasks
 from spyder.widgets.tabs import BaseTabs
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.plugins.explorer.widgets.explorer import (
     show_in_external_file_explorer)
 
diff --git a/spyder/plugins/editor/widgets/tests/conftest.py b/spyder/plugins/editor/widgets/tests/conftest.py
index 7db2449a008..e77694ca375 100644
--- a/spyder/plugins/editor/widgets/tests/conftest.py
+++ b/spyder/plugins/editor/widgets/tests/conftest.py
@@ -20,7 +20,7 @@
 from qtpy.QtGui import QFont
 
 # Local imports
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.plugins.editor.tests.conftest import (
     editor_plugin, editor_plugin_open_files, python_files)
 from spyder.plugins.completion.languageserver.tests.conftest import (
diff --git a/spyder/plugins/editor/widgets/tests/test_warnings.py b/spyder/plugins/editor/widgets/tests/test_warnings.py
index 3e5de792b23..db992776f41 100644
--- a/spyder/plugins/editor/widgets/tests/test_warnings.py
+++ b/spyder/plugins/editor/widgets/tests/test_warnings.py
@@ -15,7 +15,7 @@
 from qtpy.QtCore import Qt
 
 # Local imports
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 
 
 TEXT = ("def some_function():\n"  # D100, D103: Missing docstring
diff --git a/spyder/plugins/explorer/widgets/fileassociations.py b/spyder/plugins/explorer/widgets/fileassociations.py
index b4a5d1f365b..bf8856a57b7 100644
--- a/spyder/plugins/explorer/widgets/fileassociations.py
+++ b/spyder/plugins/explorer/widgets/fileassociations.py
@@ -440,7 +440,7 @@ def load_values(self, data=None):
         `/path/to/app/executable` is an executable app on mac and windows and
         a .desktop xdg file on linux.
         """
-        self._data = data
+        self._data = {} if data is None else data
         self._update_extensions()
 
     def add_association(self, value=None):
diff --git a/spyder/plugins/help/utils/conf.py b/spyder/plugins/help/utils/conf.py
index 9dd659f0a50..c3b14b82746 100644
--- a/spyder/plugins/help/utils/conf.py
+++ b/spyder/plugins/help/utils/conf.py
@@ -19,7 +19,7 @@
 from sphinx import __version__ as sphinx_version
 
 # Local imports
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 
 
 #==============================================================================
diff --git a/spyder/plugins/ipythonconsole/plugin.py b/spyder/plugins/ipythonconsole/plugin.py
index 45e5d29873c..1e6ca9fe887 100644
--- a/spyder/plugins/ipythonconsole/plugin.py
+++ b/spyder/plugins/ipythonconsole/plugin.py
@@ -38,7 +38,7 @@
 from spyder import dependencies
 from spyder.config.base import _, get_conf_path, get_home_dir
 from spyder.config.gui import get_font, is_dark_interface
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.api.plugins import SpyderPluginWidget
 from spyder.py3compat import is_string, PY2, to_text_string
 from spyder.plugins.ipythonconsole.confpage import IPythonConsoleConfigPage
diff --git a/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py b/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py
index ca9a41b904b..868a7da3416 100644
--- a/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py
+++ b/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py
@@ -37,7 +37,7 @@
 
 # Local imports
 from spyder.config.gui import get_color_scheme
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.py3compat import PY2, to_text_string
 from spyder.plugins.help.tests.test_plugin import check_text
 from spyder.plugins.ipythonconsole.plugin import IPythonConsole
diff --git a/spyder/plugins/ipythonconsole/utils/kernelspec.py b/spyder/plugins/ipythonconsole/utils/kernelspec.py
index 46827db9d23..5ed01a9b081 100644
--- a/spyder/plugins/ipythonconsole/utils/kernelspec.py
+++ b/spyder/plugins/ipythonconsole/utils/kernelspec.py
@@ -15,7 +15,7 @@
 
 from spyder.config.base import (SAFE_MODE, get_module_source_path,
                                 running_under_pytest)
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.utils.encoding import to_unicode_from_fs
 from spyder.utils.programs import is_python_interpreter
 from spyder.py3compat import PY2, iteritems, to_text_string, to_binary_string
diff --git a/spyder/plugins/ipythonconsole/utils/tests/test_spyder_kernel.py b/spyder/plugins/ipythonconsole/utils/tests/test_spyder_kernel.py
index e06a3c6f488..c149855233e 100644
--- a/spyder/plugins/ipythonconsole/utils/tests/test_spyder_kernel.py
+++ b/spyder/plugins/ipythonconsole/utils/tests/test_spyder_kernel.py
@@ -11,7 +11,7 @@
 import os
 import pytest
 
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.py3compat import PY2, is_binary_string, to_text_string
 from spyder.utils.encoding import to_fs_from_unicode
 from spyder.plugins.ipythonconsole.utils.kernelspec import SpyderKernelSpec
diff --git a/spyder/plugins/ipythonconsole/widgets/debugging.py b/spyder/plugins/ipythonconsole/widgets/debugging.py
index 2a7247e274c..6a9f9a9c3bd 100644
--- a/spyder/plugins/ipythonconsole/widgets/debugging.py
+++ b/spyder/plugins/ipythonconsole/widgets/debugging.py
@@ -17,7 +17,7 @@
 from qtconsole.rich_jupyter_widget import RichJupyterWidget
 
 from spyder.config.base import PICKLE_PROTOCOL
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.py3compat import to_text_string
 
 
diff --git a/spyder/plugins/ipythonconsole/widgets/kernelconnect.py b/spyder/plugins/ipythonconsole/widgets/kernelconnect.py
index d4f73eed895..5a5116382ff 100644
--- a/spyder/plugins/ipythonconsole/widgets/kernelconnect.py
+++ b/spyder/plugins/ipythonconsole/widgets/kernelconnect.py
@@ -22,7 +22,7 @@
 
 # Local imports
 from spyder.config.base import _, get_home_dir
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 
 
 class KernelConnectionDialog(QDialog):
diff --git a/spyder/plugins/ipythonconsole/widgets/shell.py b/spyder/plugins/ipythonconsole/widgets/shell.py
index b4d82187433..5aa678b338c 100644
--- a/spyder/plugins/ipythonconsole/widgets/shell.py
+++ b/spyder/plugins/ipythonconsole/widgets/shell.py
@@ -15,7 +15,7 @@
 
 from qtpy.QtCore import Signal
 from qtpy.QtWidgets import QMessageBox
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.config.base import _
 from spyder.config.gui import config_shortcut
 from spyder.py3compat import PY2, to_text_string
diff --git a/spyder/plugins/ipythonconsole/widgets/tests/test_kernelconnect.py b/spyder/plugins/ipythonconsole/widgets/tests/test_kernelconnect.py
index 2c3ee278fe6..563fb4b1e57 100644
--- a/spyder/plugins/ipythonconsole/widgets/tests/test_kernelconnect.py
+++ b/spyder/plugins/ipythonconsole/widgets/tests/test_kernelconnect.py
@@ -19,7 +19,7 @@
 # Local imports
 from spyder.plugins.ipythonconsole.widgets.kernelconnect import (
     KernelConnectionDialog)
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 
 
 # =============================================================================
diff --git a/spyder/plugins/projects/api.py b/spyder/plugins/projects/api.py
index 5f6f968faa8..5e384dd220b 100644
--- a/spyder/plugins/projects/api.py
+++ b/spyder/plugins/projects/api.py
@@ -11,7 +11,8 @@
 import os.path as osp
 from collections import OrderedDict
 
-from spyder.config.base import _
+from spyder.config.base import _, get_project_config_folder
+from spyder.config.user import UserConfig
 from spyder.py3compat import to_text_string
 from spyder.plugins.projects.utils.config import (ProjectConfig, CODESTYLE,
                                             CODESTYLE_DEFAULTS,
@@ -29,7 +30,6 @@ class BaseProject(object):
     This base class must not be used directly, but inherited from. It does not
     assume that python is specific to this project.
     """
-    PROJECT_FOLDER = '.spyproject'
     PROJECT_TYPE_NAME = None
     IGNORE_FILE = ""
     CONFIG_SETUP = {WORKSPACE: {'filename': '{0}.ini'.format(WORKSPACE),
@@ -97,14 +97,14 @@ def get_recent_files(self):
     def create_project_config_files(self):
         """ """
         dic = self.CONFIG_SETUP
+        path = osp.join(self.root_path, get_project_config_folder(), 'config')
+
         for key in dic:
             name = key
-            filename = dic[key]['filename']
             defaults = dic[key]['defaults']
             version = dic[key]['version']
-            self.CONF[key] = ProjectConfig(name, self.root_path, filename,
-                                           defaults=defaults, load=True,
-                                           version=version)
+            self.CONF[key] = UserConfig(name, path, defaults=defaults,
+                                        load=True, version=version)
 
     def get_conf_files(self):
         """ """
@@ -141,7 +141,7 @@ def rename(self, new_name):
 
     def __get_project_config_folder(self):
         """Return project configuration folder."""
-        return osp.join(self.root_path, self.PROJECT_FOLDER)
+        return osp.join(self.root_path, get_project_config_folder())
 
     def __get_project_config_path(self):
         """Return project configuration path"""
diff --git a/spyder/plugins/projects/plugin.py b/spyder/plugins/projects/plugin.py
index 720ff8c79cb..42a64fb4410 100644
--- a/spyder/plugins/projects/plugin.py
+++ b/spyder/plugins/projects/plugin.py
@@ -23,7 +23,7 @@
 
 # Local imports
 from spyder.config.base import _, get_home_dir
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.api.plugins import SpyderPluginWidget
 from spyder.py3compat import is_text_string, to_text_string
 from spyder.utils import encoding
diff --git a/spyder/plugins/projects/utils/config.py b/spyder/plugins/projects/utils/config.py
index c0fcf1cd7c6..1be5ac30741 100644
--- a/spyder/plugins/projects/utils/config.py
+++ b/spyder/plugins/projects/utils/config.py
@@ -11,10 +11,10 @@
 import os
 
 # Local imports
+from spyder.config.base import get_project_config_folder
 from spyder.config.user import UserConfig
 
 PROJECT_FILENAME = '.spyproj'
-PROJECT_FOLDER = '.spyproject'
 
 
 # Project configuration defaults
@@ -61,40 +61,4 @@
 
 
 class ProjectConfig(UserConfig):
-    """ProjectConfig class, based on UserConfig.
-
-    Parameters
-    ----------
-    name: str
-        name of the config
-    defaults: tuple
-        dictionnary containing options *or* list of tuples
-        (section_name, options)
-    version: str
-        version of the configuration file (X.Y.Z format)
-    filename: str
-        configuration file will be saved in %home%/subfolder/%name%.ini
-    """
-    DEFAULT_SECTION_NAME = 'main'
-
-    def __init__(self, name, root_path, filename, defaults=None, load=True,
-                 version=None):
-        self.project_root_path = root_path
-
-        # Config rootpath
-        self._root_path = os.path.join(root_path, PROJECT_FOLDER)
-        self._filename = filename
-
-        # Create folder if non existent
-        if not os.path.isdir(self._root_path):
-            os.makedirs(self._root_path)
-
-        # Add file
-        # NOTE: We have to think better about the uses of this file
-        # with open(os.path.join(root_path, PROJECT_FILENAME), 'w') as f:
-        #    f.write('spyder-ide project\n')
-
-        UserConfig.__init__(self, name, defaults=defaults, load=load,
-                            version=version, subfolder=None, backup=False,
-                            raw_mode=True, remove_obsolete=True)
-
+    pass
diff --git a/spyder/plugins/projects/widgets/tests/test_project.py b/spyder/plugins/projects/widgets/tests/test_project.py
index 68babca6f9d..a5d6f302539 100644
--- a/spyder/plugins/projects/widgets/tests/test_project.py
+++ b/spyder/plugins/projects/widgets/tests/test_project.py
@@ -7,13 +7,15 @@
 Tests for __init__.py.
 """
 
-# Test library imports
-import pytest
+# Standard library imports
+import os
 import os.path as osp
 
+# Third party imports
+import pytest
+
 # Local imports
 from spyder.plugins.projects.api import EmptyProject
-
 from spyder.plugins.projects.utils.config import (CODESTYLE, WORKSPACE,
                                                   ENCODING, VCS)
 
@@ -28,23 +30,31 @@ def project_test(tmpdir_factory):
         project: EmptyProject object.
     """
     project_dir = tmpdir_factory.mktemp("test_project")
+    os.makedirs(osp.join(str(project_dir), '.spyproject', 'config'))
     project = EmptyProject(str(project_dir))
     return project_dir, project
 
 
-def test_empty_project(project_test):
-    """Test creation of anEmpy project, and its configuration files."""
+def test_empty_project(project_test, qtbot):
+    """Test creation of an Empy project, and its configuration files."""
     project_dir, project = project_test
     assert project.root_path == str(project_dir)
 
-    # Assert Project onfigs
+    print(project.root_path, os.listdir(osp.join(project.root_path,
+                                        '.spyproject', 'config')))
+
+    # Assert Project configs
     conf_files = project.get_conf_files()
-    for dir_ in [CODESTYLE, WORKSPACE, ENCODING, VCS]:
-        assert dir_ in conf_files
-        project_config = conf_files[dir_]
+
+    qtbot.wait(3000)
+    for filename in [CODESTYLE, ENCODING, VCS]:
+        assert filename in conf_files
+        project_config = conf_files[filename]
 
         # assert configurations files
-        assert osp.exists(project_config.filename())
+        fpath = project_config.get_config_fpath()
+        print([fpath])
+        assert osp.isfile(fpath)
 
 
 def test_set_load_recent_files(project_test):
diff --git a/spyder/plugins/variableexplorer/widgets/collectionseditor.py b/spyder/plugins/variableexplorer/widgets/collectionseditor.py
index 1b2a6a0bfcf..60b90d12361 100644
--- a/spyder/plugins/variableexplorer/widgets/collectionseditor.py
+++ b/spyder/plugins/variableexplorer/widgets/collectionseditor.py
@@ -1532,7 +1532,7 @@ def remote_editor_test():
     from spyder.utils.qthelpers import qapplication
     app = qapplication()
 
-    from spyder.config.main import CONF
+    from spyder.config.manager import CONF
     from spyder_kernels.utils.nsview import (make_remote_view,
                                              REMOTE_SETTINGS)
 
diff --git a/spyder/plugins/variableexplorer/widgets/namespacebrowser.py b/spyder/plugins/variableexplorer/widgets/namespacebrowser.py
index ca64098af54..c3ed135b6a9 100644
--- a/spyder/plugins/variableexplorer/widgets/namespacebrowser.py
+++ b/spyder/plugins/variableexplorer/widgets/namespacebrowser.py
@@ -27,7 +27,7 @@
 
 # Local imports
 from spyder.config.base import _
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.py3compat import is_text_string, to_text_string
 from spyder.utils import encoding
 from spyder.utils import icon_manager as ima
diff --git a/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py b/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py
index d744310d20c..f83a845ea34 100644
--- a/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py
+++ b/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py
@@ -26,7 +26,7 @@
 # Local imports
 from spyder.config.base import _
 from spyder.config.gui import get_font, is_dark_interface
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.utils.qthelpers import (add_actions, create_plugin_layout,
                                     create_toolbutton, qapplication)
 from spyder.plugins.editor.widgets.codeeditor import CodeEditor
diff --git a/spyder/plugins/workingdirectory/tests/test_workingdirectory.py b/spyder/plugins/workingdirectory/tests/test_workingdirectory.py
index 70f79b3a75a..95e4584488e 100644
--- a/spyder/plugins/workingdirectory/tests/test_workingdirectory.py
+++ b/spyder/plugins/workingdirectory/tests/test_workingdirectory.py
@@ -13,8 +13,7 @@
 # Local imports
 from spyder.plugins.workingdirectory.plugin import WorkingDirectory
 from spyder.config.base import get_home_dir
-from spyder.config.main import CONF
-
+from spyder.config.manager import CONF
 
 NEW_DIR = 'new_workingdir'
 
diff --git a/spyder/preferences/appearance.py b/spyder/preferences/appearance.py
index 981d4dd661f..1bbc7ea9f46 100644
--- a/spyder/preferences/appearance.py
+++ b/spyder/preferences/appearance.py
@@ -15,7 +15,7 @@
 from spyder.config.base import _
 from spyder.config.gui import (get_font, set_font, is_dark_font_color,
                                is_dark_interface)
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.config.utils import is_gtk_desktop
 from spyder.plugins.editor.widgets.codeeditor import CodeEditor
 from spyder.preferences.configdialog import GeneralConfigPage
diff --git a/spyder/preferences/configdialog.py b/spyder/preferences/configdialog.py
index 0c38abf6b6b..5e039a14c74 100644
--- a/spyder/preferences/configdialog.py
+++ b/spyder/preferences/configdialog.py
@@ -29,7 +29,7 @@
 
 # Local imports
 from spyder.config.base import _, load_lang_conf
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.config.user import NoDefault
 from spyder.py3compat import to_text_string
 from spyder.utils import icon_manager as ima
diff --git a/spyder/preferences/runconfig.py b/spyder/preferences/runconfig.py
index 092f69362fe..88ec91b8cd7 100644
--- a/spyder/preferences/runconfig.py
+++ b/spyder/preferences/runconfig.py
@@ -20,7 +20,7 @@
 
 # Local imports
 from spyder.config.base import _
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.preferences.configdialog import GeneralConfigPage
 from spyder.py3compat import to_text_string
 from spyder.utils import icon_manager as ima
diff --git a/spyder/preferences/shortcuts.py b/spyder/preferences/shortcuts.py
index 301e3a52771..7b37a709e36 100644
--- a/spyder/preferences/shortcuts.py
+++ b/spyder/preferences/shortcuts.py
@@ -23,7 +23,7 @@
 
 # Local imports
 from spyder.config.base import _
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.config.gui import (get_shortcut, iter_shortcuts,
                                reset_shortcuts, set_shortcut)
 from spyder.preferences.configdialog import GeneralConfigPage
diff --git a/spyder/preferences/tests/test_shorcuts.py b/spyder/preferences/tests/test_shorcuts.py
index ab2e0495a06..ac7d873cff0 100644
--- a/spyder/preferences/tests/test_shorcuts.py
+++ b/spyder/preferences/tests/test_shorcuts.py
@@ -16,7 +16,7 @@
 from qtpy.QtCore import Qt
 
 # Local imports
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.preferences.shortcuts import (
     ShortcutsTable, ShortcutEditor, NO_WARNING, SEQUENCE_CONFLICT,
     INVALID_KEY, SEQUENCE_EMPTY)
diff --git a/spyder/utils/fixtures.py b/spyder/utils/fixtures.py
index 36694b24f2f..164a3cf4567 100644
--- a/spyder/utils/fixtures.py
+++ b/spyder/utils/fixtures.py
@@ -4,17 +4,10 @@
 # Licensed under the terms of the MIT License
 #
 
-"""
-Testing utilities to be used with pytest.
-"""
+"""Testing utilities to be used with pytest."""
 
 # Standard library imports
 import shutil
-import tempfile
-try:
-    from unittest.mock import Mock
-except ImportError:
-    from mock import Mock # Python 2
 
 # Third party imports
 import pytest
@@ -24,23 +17,25 @@
 
 
 @pytest.fixture
-def tmpconfig(request):
-    """
-    Fixtures that returns a temporary CONF element.
-    """
-    SUBFOLDER = tempfile.mkdtemp()
-    CONF = UserConfig('spyder-test',
-                      defaults=DEFAULTS,
-                      version=CONF_VERSION,
-                      subfolder=SUBFOLDER,
-                      raw_mode=True,
-                      )
+def tmpconfig(tmpdir, request):
+    path = str(tmpdir)
+    default_kwargs = {
+        'name': 'spyder-test',
+        'path': path,
+        'defaults': DEFAULTS,
+        'load': True,
+        'version': CONF_VERSION,
+        'backup': True,
+        'raw_mode': True,
+        'remove_obsolete': False,
+    }
+
+    conf = UserConfig(**default_kwargs)
 
     def fin():
-        """
-        Fixture finalizer to delete the temporary CONF element.
-        """
-        shutil.rmtree(SUBFOLDER)
+        """Fixture finalizer to delete the temporary CONF element."""
+        shutil.rmtree(path)
 
     request.addfinalizer(fin)
-    return CONF
+
+    return conf
diff --git a/spyder/utils/icon_manager.py b/spyder/utils/icon_manager.py
index bd04d0a0dea..b4c02865eec 100644
--- a/spyder/utils/icon_manager.py
+++ b/spyder/utils/icon_manager.py
@@ -17,7 +17,7 @@
 
 # Local imports
 from spyder.config.base import get_image_path
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.config.gui import is_dark_interface
 from spyder.utils.encoding import is_text_file
 import qtawesome as qta
diff --git a/spyder/utils/syntaxhighlighters.py b/spyder/utils/syntaxhighlighters.py
index 9319bfb1a67..392998f3b31 100644
--- a/spyder/utils/syntaxhighlighters.py
+++ b/spyder/utils/syntaxhighlighters.py
@@ -29,7 +29,7 @@
 # Local imports
 from spyder import dependencies
 from spyder.config.base import _
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.py3compat import (builtins, is_text_string, to_text_string, PY3,
                               PY36_OR_MORE)
 from spyder.plugins.editor.utils.languages import CELL_LANGUAGES
diff --git a/spyder/widgets/fileswitcher.py b/spyder/widgets/fileswitcher.py
index 64bc9b4d3bc..7b25b59d1d7 100644
--- a/spyder/widgets/fileswitcher.py
+++ b/spyder/widgets/fileswitcher.py
@@ -25,7 +25,7 @@
 from spyder.utils import icon_manager as ima
 from spyder.utils.stringmatching import get_search_scores
 from spyder.widgets.helperwidgets import HelperToolButton, HTMLDelegate
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 
 
 # --- Python Outline explorer helpers
diff --git a/spyder/widgets/github/backend.py b/spyder/widgets/github/backend.py
index 0d250fd0581..4942b9f5fb2 100644
--- a/spyder/widgets/github/backend.py
+++ b/spyder/widgets/github/backend.py
@@ -27,7 +27,7 @@
 from qtpy.QtWidgets import qApp, QMessageBox
 
 
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.config.base import _, running_under_pytest
 from spyder.py3compat import PY2
 from spyder.utils.external import github
diff --git a/spyder/widgets/github/tests/test_github_backend.py b/spyder/widgets/github/tests/test_github_backend.py
index 5aeac083d8b..393c662d26c 100644
--- a/spyder/widgets/github/tests/test_github_backend.py
+++ b/spyder/widgets/github/tests/test_github_backend.py
@@ -19,7 +19,7 @@
 
 import pytest
 
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.py3compat import PY2
 from spyder.widgets.github import backend
 
diff --git a/spyder/widgets/mixins.py b/spyder/widgets/mixins.py
index 5785b48f892..4125c894f32 100644
--- a/spyder/widgets/mixins.py
+++ b/spyder/widgets/mixins.py
@@ -33,7 +33,7 @@
 # Local imports
 from spyder.config.base import _
 from spyder.config.gui import is_dark_interface
-from spyder.config.main import CONF
+from spyder.config.manager import CONF
 from spyder.py3compat import is_text_string, to_text_string
 from spyder.utils import encoding, sourcecode, programs
 from spyder.utils.misc import get_error_match


Code review comments:
diff_hunk: @@ -1,23 +1,23 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright © Spyder Project Contributors
+# # Copyright © Spyder Project Contributors
path: spyder/config/main.py
text: @user1:
Please remove the extra `#` here.

@author:
Fixed!

diff_hunk: @@ -5,369 +5,498 @@
 # (see spyder/__init__.py for details)
 
 """
-This module provides user configuration file management features for Spyder
+This module provides user configuration file management features for Spyder.
 
-It's based on the ConfigParser module (present in the standard library).
+It is based on the ConfigParser module present in the standard library.
 """
 
-from __future__ import print_function
+from __future__ import print_function, unicode_literals
 
-# Std imports
+# Standard library imports
 import ast
+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_home_dir,
-                                get_module_source_path)
-from spyder.utils.programs import check_version
+from spyder.config.base import get_conf_path, get_module_source_path
 from spyder.py3compat import configparser as cp
-from spyder.py3compat import PY2, is_text_string, to_text_string
-
-# Std imports for Python 2
-if PY2:
-    import codecs
+from spyder.py3compat import is_text_string, PY2, to_text_string
+from spyder.utils.programs import check_version
 
 
-#==============================================================================
+# ============================================================================
 # Auxiliary classes
-#==============================================================================
+# ============================================================================
 class NoDefault:
     pass
 
 
-#==============================================================================
+# ============================================================================
 # Defaults class
-#==============================================================================
-class DefaultsConfig(cp.ConfigParser):
+# ============================================================================
+class DefaultsConfig(cp.ConfigParser, object):
     """
-    Class used to save defaults to a file and as base class for
-    UserConfig
+    Class used to save defaults to a file and as UserConfig base class.
     """
-    def __init__(self, name, subfolder):
+
+    def __init__(self, name, path):
+        """
+        Class used to save defaults to a file and as UserConfig base class.
+        """
         if PY2:
-            cp.ConfigParser.__init__(self)
+            super(DefaultsConfig, self).__init__()
         else:
-            cp.ConfigParser.__init__(self, interpolation=None)
-        self.name = name
-        self.subfolder = subfolder
+            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):
         """
-        Private write method for Python 2
-        The one from configparser fails for non-ascii Windows accounts
+        Write method for Python 2.
+
+        The one from configparser fails for non-ascii Windows accounts.
         """
         if self._defaults:
-            fp.write("[%s]\n" % cp.DEFAULTSECT)
+            fp.write('[{}]\n'.format(cp.DEFAULTSECT))
             for (key, value) in self._defaults.items():
-                fp.write("%s = %s\n" % (key, str(value).replace('\n', '\n\t')))
-            fp.write("\n")
+                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("[%s]\n" % section)
+            fp.write('[{}]\n'.format(section))
             for (key, value) in self._sections[section].items():
-                if key == "__name__":
+                if key == '__name__':
                     continue
+
                 if (value is not None) or (self._optcre == self.OPTCRE):
                     value = to_text_string(value)
-                    key = " = ".join((key, value.replace('\n', '\n\t')))
-                fp.write("%s\n" % (key))
-            fp.write("\n")
+                    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):
-        """
-        Private set method
-        """
+        """Set method."""
         if not self.has_section(section):
-            self.add_section( section )
+            self.add_section(section)
+
         if not is_text_string(value):
-            value = repr( value )
+            value = repr(value)
+
         if verbose:
-            print('%s[ %s ] = %s' % (section, option, value))  # spyder: test-skip
-        cp.ConfigParser.set(self, section, option, value)
+            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
-        """
-        # See spyder-ide/spyder#1086 and spyder-ide/spyder#1242 for background
-        # on why this method contains all the exception handling.
-        fname = self.filename()
+        """Save config into the associated .ini file."""
+        fpath = self.get_config_fpath()
 
-        def _write_file(fname):
-            if PY2:
-                # Python 2
-                with codecs.open(fname, 'w', encoding='utf-8') as configfile:
+        def _write_file(fpath):
+            with io.open(fpath, 'w', encoding='utf-8') as configfile:
+                if PY2:
                     self._write(configfile)
-            else:
-                # Python 3
-                with open(fname, 'w', encoding='utf-8') as configfile:
+                else:
                     self.write(configfile)
 
-        try:  # the "easy" way
-            _write_file(fname)
+        # 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(fname):
-                    os.remove(fname)
+            try:
+                # The "delete and sleep" way
+                if osp.isfile(fpath):
+                    os.remove(fpath)
+
                 time.sleep(0.05)
-                _write_file(fname)
+                _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('Failed to write user configuration file to disk, with '
+                      'the exception shown below')  # spyder: test-skip
                 print(e)  # spyder: test-skip
 
-    def filename(self):
-        """Defines the name of the configuration file to use."""
-        # Needs to be done this way to be used by the project config.
-        # To fix on a later PR
-        self._filename = getattr(self, '_filename', None)
-        self._root_path = getattr(self, '_root_path', None)
-
-        if self._filename is None and self._root_path is None:
-            return self._filename_global()
-        else:
-            return self._filename_projects()
-
-    def _filename_projects(self):
-        """Create a .ini filename located in the current project directory.
-        This .ini files stores the specific project preferences for each
-        project created with spyder.
-        """
-        return osp.join(self._root_path, self._filename)
-
-    def _filename_global(self):
-        """Create a .ini filename located in user home directory.
-        This .ini files stores the global spyder preferences.
-        """
-        if self.subfolder is None:
-            config_file = osp.join(get_home_dir(), '.%s.ini' % self.name)
-            return config_file
-        else:
-            folder = get_conf_path()
-            # Save defaults in a "defaults" dir of .spyder2 to not pollute it
-            if 'defaults' in self.name:
-                folder = osp.join(folder, 'defaults')
-                if not osp.isdir(folder):
-                    os.mkdir(folder)
-            config_file = osp.join(folder, '%s.ini' % self.name)
-            return config_file
+    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, False)
+                new_value = options[option]
+                self._set(section, option, new_value, verbose=False)
 
 
-#==============================================================================
+# ============================================================================
 # User config class
-#==============================================================================
+# ============================================================================
 class UserConfig(DefaultsConfig):
     """
-    UserConfig class, based on ConfigParser
-    name: name of the config
-    defaults: dictionnary containing options
-              *or* list of tuples (section_name, options)
-    version: version of the configuration file (X.Y.Z format)
-    subfolder: configuration file will be saved in %home%/subfolder/%name%.ini
-
-    Note that 'get' and 'set' arguments number and type
-    differ from the overriden methods
+    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, {}),]
+        dictionnary containing options *or* list of tuples (sec_name, options)
path: spyder/config/user.py
text: @user1:
dictionnary -> dictionary

@author:
Fixed

diff_hunk: @@ -5,369 +5,498 @@
 # (see spyder/__init__.py for details)
 
 """
-This module provides user configuration file management features for Spyder
+This module provides user configuration file management features for Spyder.
 
-It's based on the ConfigParser module (present in the standard library).
+It is based on the ConfigParser module present in the standard library.
 """
 
-from __future__ import print_function
+from __future__ import print_function, unicode_literals
 
-# Std imports
+# Standard library imports
 import ast
+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_home_dir,
-                                get_module_source_path)
-from spyder.utils.programs import check_version
+from spyder.config.base import get_conf_path, get_module_source_path
 from spyder.py3compat import configparser as cp
-from spyder.py3compat import PY2, is_text_string, to_text_string
-
-# Std imports for Python 2
-if PY2:
-    import codecs
+from spyder.py3compat import is_text_string, PY2, to_text_string
+from spyder.utils.programs import check_version
 
 
-#==============================================================================
+# ============================================================================
 # Auxiliary classes
-#==============================================================================
+# ============================================================================
 class NoDefault:
     pass
 
 
-#==============================================================================
+# ============================================================================
 # Defaults class
-#==============================================================================
-class DefaultsConfig(cp.ConfigParser):
+# ============================================================================
+class DefaultsConfig(cp.ConfigParser, object):
     """
-    Class used to save defaults to a file and as base class for
-    UserConfig
+    Class used to save defaults to a file and as UserConfig base class.
     """
-    def __init__(self, name, subfolder):
+
+    def __init__(self, name, path):
+        """
+        Class used to save defaults to a file and as UserConfig base class.
+        """
         if PY2:
-            cp.ConfigParser.__init__(self)
+            super(DefaultsConfig, self).__init__()
         else:
-            cp.ConfigParser.__init__(self, interpolation=None)
-        self.name = name
-        self.subfolder = subfolder
+            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):
         """
-        Private write method for Python 2
-        The one from configparser fails for non-ascii Windows accounts
+        Write method for Python 2.
+
+        The one from configparser fails for non-ascii Windows accounts.
         """
         if self._defaults:
-            fp.write("[%s]\n" % cp.DEFAULTSECT)
+            fp.write('[{}]\n'.format(cp.DEFAULTSECT))
             for (key, value) in self._defaults.items():
-                fp.write("%s = %s\n" % (key, str(value).replace('\n', '\n\t')))
-            fp.write("\n")
+                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("[%s]\n" % section)
+            fp.write('[{}]\n'.format(section))
             for (key, value) in self._sections[section].items():
-                if key == "__name__":
+                if key == '__name__':
                     continue
+
                 if (value is not None) or (self._optcre == self.OPTCRE):
                     value = to_text_string(value)
-                    key = " = ".join((key, value.replace('\n', '\n\t')))
-                fp.write("%s\n" % (key))
-            fp.write("\n")
+                    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):
-        """
-        Private set method
-        """
+        """Set method."""
         if not self.has_section(section):
-            self.add_section( section )
+            self.add_section(section)
+
         if not is_text_string(value):
-            value = repr( value )
+            value = repr(value)
+
         if verbose:
-            print('%s[ %s ] = %s' % (section, option, value))  # spyder: test-skip
-        cp.ConfigParser.set(self, section, option, value)
+            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
-        """
-        # See spyder-ide/spyder#1086 and spyder-ide/spyder#1242 for background
-        # on why this method contains all the exception handling.
-        fname = self.filename()
+        """Save config into the associated .ini file."""
+        fpath = self.get_config_fpath()
 
-        def _write_file(fname):
-            if PY2:
-                # Python 2
-                with codecs.open(fname, 'w', encoding='utf-8') as configfile:
+        def _write_file(fpath):
+            with io.open(fpath, 'w', encoding='utf-8') as configfile:
+                if PY2:
                     self._write(configfile)
-            else:
-                # Python 3
-                with open(fname, 'w', encoding='utf-8') as configfile:
+                else:
                     self.write(configfile)
 
-        try:  # the "easy" way
-            _write_file(fname)
+        # 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(fname):
-                    os.remove(fname)
+            try:
+                # The "delete and sleep" way
+                if osp.isfile(fpath):
+                    os.remove(fpath)
+
                 time.sleep(0.05)
-                _write_file(fname)
+                _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('Failed to write user configuration file to disk, with '
+                      'the exception shown below')  # spyder: test-skip
                 print(e)  # spyder: test-skip
 
-    def filename(self):
-        """Defines the name of the configuration file to use."""
-        # Needs to be done this way to be used by the project config.
-        # To fix on a later PR
-        self._filename = getattr(self, '_filename', None)
-        self._root_path = getattr(self, '_root_path', None)
-
-        if self._filename is None and self._root_path is None:
-            return self._filename_global()
-        else:
-            return self._filename_projects()
-
-    def _filename_projects(self):
-        """Create a .ini filename located in the current project directory.
-        This .ini files stores the specific project preferences for each
-        project created with spyder.
-        """
-        return osp.join(self._root_path, self._filename)
-
-    def _filename_global(self):
-        """Create a .ini filename located in user home directory.
-        This .ini files stores the global spyder preferences.
-        """
-        if self.subfolder is None:
-            config_file = osp.join(get_home_dir(), '.%s.ini' % self.name)
-            return config_file
-        else:
-            folder = get_conf_path()
-            # Save defaults in a "defaults" dir of .spyder2 to not pollute it
-            if 'defaults' in self.name:
-                folder = osp.join(folder, 'defaults')
-                if not osp.isdir(folder):
-                    os.mkdir(folder)
-            config_file = osp.join(folder, '%s.ini' % self.name)
-            return config_file
+    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, False)
+                new_value = options[option]
+                self._set(section, option, new_value, verbose=False)
 
 
-#==============================================================================
+# ============================================================================
 # User config class
-#==============================================================================
+# ============================================================================
 class UserConfig(DefaultsConfig):
     """
-    UserConfig class, based on ConfigParser
-    name: name of the config
-    defaults: dictionnary containing options
-              *or* list of tuples (section_name, options)
-    version: version of the configuration file (X.Y.Z format)
-    subfolder: configuration file will be saved in %home%/subfolder/%name%.ini
-
-    Note that 'get' and 'set' arguments number and type
-    differ from the overriden methods
+    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, {}),]
+        dictionnary containing options *or* list of tuples (sec_name, options)
+    load: bool
+        TODO:
+    version: str
+        version of the configuration file in 'major.minor.micro' format.
+    backup: bool
+        TODO:
+    raw_mode: bool
+        TODO:
+    remove_obsolete: bool
+        TODO:
path: spyder/config/user.py
text: @user1:
Please fill the TODOs you left in this docstring.

@author:
Done!

diff_hunk: @@ -0,0 +1,168 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+Configuration manager providing access to user/site/project configuration.
+"""
+
+# Standard library imports
+import os
+import os.path as osp
+
+# Local imports
+from spyder.config.base import get_conf_path, get_conf_subfolder, get_home_dir
+from spyder.config.main import CONF_VERSION, DEFAULTS, NAME_MAP
+from spyder.config.user import MultiUserConfig, NoDefault
+
+
+class ConfigurationManager(object):
+    """
+    Configuration manager to provide access to user/site/project config.
+    """
+
+    def __init__(self, parent=None, active_project_callback=None):
+        """
+        Configuration manager to provide access to user/site/project config.
+        """
+        path = self.get_user_config_path()
+        if not osp.isdir(path):
+            os.makedirs(path)
+
+        self._parent = parent
+        self._active_project_callback = active_project_callback
+        self._user_config = MultiUserConfig(
+            NAME_MAP,
+            path=path,
+            defaults=DEFAULTS,
+            load=True,
+            version=CONF_VERSION,
+            backup=True,
+            raw_mode=True,
+            remove_obsolete=False,
+        )
+        # TODO: Implementation to be defined
+        self._site_config = None
+
+        # TODO: To be implemented in the following PR
+        self._project_configs = {}  # Cache project configurations
+
+        # Setup
+        self.remove_deprecated_config_locations()
+
+    def remove_deprecated_config_locations(self):
+        """Removing old .spyder.ini location."""
+        old_location = osp.join(get_home_dir(), '.spyder.ini')
+        if osp.isfile(old_location):
+            os.remove(old_location)
+
+    def get_system_config_path(self):
+        """Return the system configuration path."""
+        return None
+
+    def get_active_conf(self):
+        """
+        Return the active project configuration or the user configurarion.
+        """
+        # TODO: implement project configuration on the following PR
+        return self._user_config
+
+    def get_user_config_path(self):
+        """Return the user configuration path."""
+        base_path = get_conf_path()
+        path = osp.join(base_path, 'app', 'config')
+        if not osp.isdir(path):
+            os.makedirs(path)
+
+        return path
+
+    # --- Projects
+    # ------------------------------------------------------------------------
+    def register_config(self, root_path, config):
+        """
+        Register confgiuration with `root_path`.
path: spyder/config/manager.py
text: @user1:
confgiuration -> configuration

diff_hunk: @@ -5,369 +5,498 @@
 # (see spyder/__init__.py for details)
 
 """
-This module provides user configuration file management features for Spyder
+This module provides user configuration file management features for Spyder.
 
-It's based on the ConfigParser module (present in the standard library).
+It is based on the ConfigParser module present in the standard library.
 """
 
-from __future__ import print_function
+from __future__ import print_function, unicode_literals
 
-# Std imports
+# Standard library imports
 import ast
+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_home_dir,
-                                get_module_source_path)
-from spyder.utils.programs import check_version
+from spyder.config.base import get_conf_path, get_module_source_path
 from spyder.py3compat import configparser as cp
-from spyder.py3compat import PY2, is_text_string, to_text_string
-
-# Std imports for Python 2
-if PY2:
-    import codecs
+from spyder.py3compat import is_text_string, PY2, to_text_string
+from spyder.utils.programs import check_version
 
 
-#==============================================================================
+# ============================================================================
 # Auxiliary classes
-#==============================================================================
+# ============================================================================
 class NoDefault:
     pass
 
 
-#==============================================================================
+# ============================================================================
 # Defaults class
-#==============================================================================
-class DefaultsConfig(cp.ConfigParser):
+# ============================================================================
+class DefaultsConfig(cp.ConfigParser, object):
     """
-    Class used to save defaults to a file and as base class for
-    UserConfig
+    Class used to save defaults to a file and as UserConfig base class.
     """
-    def __init__(self, name, subfolder):
+
+    def __init__(self, name, path):
+        """
+        Class used to save defaults to a file and as UserConfig base class.
+        """
         if PY2:
-            cp.ConfigParser.__init__(self)
+            super(DefaultsConfig, self).__init__()
         else:
-            cp.ConfigParser.__init__(self, interpolation=None)
-        self.name = name
-        self.subfolder = subfolder
+            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):
         """
-        Private write method for Python 2
-        The one from configparser fails for non-ascii Windows accounts
+        Write method for Python 2.
+
+        The one from configparser fails for non-ascii Windows accounts.
         """
         if self._defaults:
-            fp.write("[%s]\n" % cp.DEFAULTSECT)
+            fp.write('[{}]\n'.format(cp.DEFAULTSECT))
             for (key, value) in self._defaults.items():
-                fp.write("%s = %s\n" % (key, str(value).replace('\n', '\n\t')))
-            fp.write("\n")
+                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("[%s]\n" % section)
+            fp.write('[{}]\n'.format(section))
             for (key, value) in self._sections[section].items():
-                if key == "__name__":
+                if key == '__name__':
                     continue
+
                 if (value is not None) or (self._optcre == self.OPTCRE):
                     value = to_text_string(value)
-                    key = " = ".join((key, value.replace('\n', '\n\t')))
-                fp.write("%s\n" % (key))
-            fp.write("\n")
+                    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):
-        """
-        Private set method
-        """
+        """Set method."""
         if not self.has_section(section):
-            self.add_section( section )
+            self.add_section(section)
+
         if not is_text_string(value):
-            value = repr( value )
+            value = repr(value)
+
         if verbose:
-            print('%s[ %s ] = %s' % (section, option, value))  # spyder: test-skip
-        cp.ConfigParser.set(self, section, option, value)
+            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
-        """
-        # See spyder-ide/spyder#1086 and spyder-ide/spyder#1242 for background
-        # on why this method contains all the exception handling.
-        fname = self.filename()
+        """Save config into the associated .ini file."""
+        fpath = self.get_config_fpath()
 
-        def _write_file(fname):
-            if PY2:
-                # Python 2
-                with codecs.open(fname, 'w', encoding='utf-8') as configfile:
+        def _write_file(fpath):
+            with io.open(fpath, 'w', encoding='utf-8') as configfile:
+                if PY2:
                     self._write(configfile)
-            else:
-                # Python 3
-                with open(fname, 'w', encoding='utf-8') as configfile:
+                else:
                     self.write(configfile)
 
-        try:  # the "easy" way
-            _write_file(fname)
+        # 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(fname):
-                    os.remove(fname)
+            try:
+                # The "delete and sleep" way
+                if osp.isfile(fpath):
+                    os.remove(fpath)
+
                 time.sleep(0.05)
-                _write_file(fname)
+                _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('Failed to write user configuration file to disk, with '
+                      'the exception shown below')  # spyder: test-skip
                 print(e)  # spyder: test-skip
 
-    def filename(self):
-        """Defines the name of the configuration file to use."""
-        # Needs to be done this way to be used by the project config.
-        # To fix on a later PR
-        self._filename = getattr(self, '_filename', None)
-        self._root_path = getattr(self, '_root_path', None)
-
-        if self._filename is None and self._root_path is None:
-            return self._filename_global()
-        else:
-            return self._filename_projects()
-
-    def _filename_projects(self):
-        """Create a .ini filename located in the current project directory.
-        This .ini files stores the specific project preferences for each
-        project created with spyder.
-        """
-        return osp.join(self._root_path, self._filename)
-
-    def _filename_global(self):
-        """Create a .ini filename located in user home directory.
-        This .ini files stores the global spyder preferences.
-        """
-        if self.subfolder is None:
-            config_file = osp.join(get_home_dir(), '.%s.ini' % self.name)
-            return config_file
-        else:
-            folder = get_conf_path()
-            # Save defaults in a "defaults" dir of .spyder2 to not pollute it
-            if 'defaults' in self.name:
-                folder = osp.join(folder, 'defaults')
-                if not osp.isdir(folder):
-                    os.mkdir(folder)
-            config_file = osp.join(folder, '%s.ini' % self.name)
-            return config_file
+    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, False)
+                new_value = options[option]
+                self._set(section, option, new_value, verbose=False)
 
 
-#==============================================================================
+# ============================================================================
 # User config class
-#==============================================================================
+# ============================================================================
 class UserConfig(DefaultsConfig):
     """
-    UserConfig class, based on ConfigParser
-    name: name of the config
-    defaults: dictionnary containing options
-              *or* list of tuples (section_name, options)
-    version: version of the configuration file (X.Y.Z format)
-    subfolder: configuration file will be saved in %home%/subfolder/%name%.ini
-
-    Note that 'get' and 'set' arguments number and type
-    differ from the overriden methods
+    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, {}),]
+        dictionnary containing options *or* list of tuples (sec_name, options)
+    load: bool
+        TODO:
+    version: str
+        version of the configuration file in 'major.minor.micro' format.
+    backup: bool
+        TODO:
+    raw_mode: bool
+        TODO:
+    remove_obsolete: bool
+        TODO:
+
+    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, defaults=None, load=True, version=None,
-                 subfolder=None, backup=False, raw_mode=False,
-                 remove_obsolete=False):
-        DefaultsConfig.__init__(self, name, subfolder)
-        self.raw = 1 if raw_mode else 0
-        if (version is not None and
-                re.match(r'^(\d+).(\d+).(\d+)$', version) is None):
-            raise ValueError("Version number %r is incorrect - must be in X.Y.Z format" % version)
-        if isinstance(defaults, dict):
-            defaults = [ (self.DEFAULT_SECTION_NAME, defaults) ]
-        self.defaults = defaults
-        if defaults is not None:
-            self.reset_to_defaults(save=False)
-        fname = self.filename()
-        if backup:
-            try:
-                shutil.copyfile(fname, "%s.bak" % fname)
-            except IOError:
-                pass
+
+    def __init__(self, name, path, defaults=None, load=True, version=None,
+                 backup=False, raw_mode=False, remove_obsolete=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._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 load:
-            # If config file already exists, it overrides Default options:
-            self.load_from_ini()
-            old_ver = self.get_version(version)
-            _major = lambda _t: _t[:_t.find('.')]
-            _minor = lambda _t: _t[:_t.rfind('.')]
+            # 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(defaults, version, subfolder)
+            self._save_new_defaults(defaults)
+
             # Updating defaults only if major/minor version is different
-            if _minor(version) != _minor(old_ver):
+            if (self._get_minor_version(version=version)
+                    != self._get_minor_version(version=old_version)):
path: spyder/config/user.py
text: @user1:
Let's remove the `version` kwarg here because I don't think it's necessary. So this should be

    if (self._get_minor_version(version)
            != self._get_minor_version(old_version):

@author:
Agreed

diff_hunk: @@ -1,23 +1,23 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright © Spyder Project Contributors
+# # Copyright © Spyder Project Contributors
 # Licensed under the terms of the MIT License
 # (see spyder/__init__.py for details)
 
 """
-Spyder configuration options
+Spyder configuration options.
 
 Note: Leave this file free of Qt related imports, so that it can be used to
-quickly load a user config file
+quickly load a user config file.
 """
 
 import os
 import os.path as osp
 import sys
 
 # Local import
-from spyder.config.base import (CHECK_ALL, EXCLUDED_NAMES, get_home_dir,
-                                SUBFOLDER)
+from spyder.config.base import (CHECK_ALL, EXCLUDED_NAMES, get_conf_path,
+                                get_home_dir)
path: spyder/config/main.py
text: @user1:
`get_conf_path` and `get_home_dir` are no longer used here, so they should be removed.

@author:
Yes!

diff_hunk: @@ -447,14 +584,376 @@ def set(self, section, option, value, verbose=False, save=True):
             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):
-        cp.ConfigParser.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):
-        cp.ConfigParser.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())
+
+
+class SpyderUserConfig(UserConfig):
+
+    def get_previous_config_fpath(self):
+        """
+        Override method.
+
+        Return the last configuration file used if found.
+        """
+        fpath = self.get_config_fpath()
+        previous_paths = [
+            # >= 52.0.0
+            fpath,
+            # < 52.0.0
+            os.path.join(get_conf_path(), 'spyder.ini'),
+            # < ??
+        ]
+        for fpath in previous_paths:
+            print('fpath', fpath)
+            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.
+        """
+        fpath = self.get_config_fpath()
+
+        if version is None:
+            fpath = self.get_config_fpath()
+        elif check_version(version, '52.0.0', '<'):
+            fpath = osp.join(get_conf_path(), 'spyder.ini')
+        elif check_version(version, '2.4.0', '<'):
+            # TODO: Should this still be supported?
+            pass
path: spyder/config/user.py
text: @user1:
No, there's no need to support this anymore.

@author:
Ok

diff_hunk: @@ -5,369 +5,498 @@
 # (see spyder/__init__.py for details)
 
 """
-This module provides user configuration file management features for Spyder
+This module provides user configuration file management features for Spyder.
 
-It's based on the ConfigParser module (present in the standard library).
+It is based on the ConfigParser module present in the standard library.
 """
 
-from __future__ import print_function
+from __future__ import print_function, unicode_literals
 
-# Std imports
+# Standard library imports
 import ast
+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_home_dir,
-                                get_module_source_path)
-from spyder.utils.programs import check_version
+from spyder.config.base import get_conf_path, get_module_source_path
 from spyder.py3compat import configparser as cp
-from spyder.py3compat import PY2, is_text_string, to_text_string
-
-# Std imports for Python 2
-if PY2:
-    import codecs
+from spyder.py3compat import is_text_string, PY2, to_text_string
+from spyder.utils.programs import check_version
 
 
-#==============================================================================
+# ============================================================================
 # Auxiliary classes
-#==============================================================================
+# ============================================================================
 class NoDefault:
     pass
 
 
-#==============================================================================
+# ============================================================================
 # Defaults class
-#==============================================================================
-class DefaultsConfig(cp.ConfigParser):
+# ============================================================================
+class DefaultsConfig(cp.ConfigParser, object):
     """
-    Class used to save defaults to a file and as base class for
-    UserConfig
+    Class used to save defaults to a file and as UserConfig base class.
     """
-    def __init__(self, name, subfolder):
+
+    def __init__(self, name, path):
+        """
+        Class used to save defaults to a file and as UserConfig base class.
+        """
         if PY2:
-            cp.ConfigParser.__init__(self)
+            super(DefaultsConfig, self).__init__()
         else:
-            cp.ConfigParser.__init__(self, interpolation=None)
-        self.name = name
-        self.subfolder = subfolder
+            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):
         """
-        Private write method for Python 2
-        The one from configparser fails for non-ascii Windows accounts
+        Write method for Python 2.
+
+        The one from configparser fails for non-ascii Windows accounts.
         """
         if self._defaults:
-            fp.write("[%s]\n" % cp.DEFAULTSECT)
+            fp.write('[{}]\n'.format(cp.DEFAULTSECT))
             for (key, value) in self._defaults.items():
-                fp.write("%s = %s\n" % (key, str(value).replace('\n', '\n\t')))
-            fp.write("\n")
+                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("[%s]\n" % section)
+            fp.write('[{}]\n'.format(section))
             for (key, value) in self._sections[section].items():
-                if key == "__name__":
+                if key == '__name__':
                     continue
+
                 if (value is not None) or (self._optcre == self.OPTCRE):
                     value = to_text_string(value)
-                    key = " = ".join((key, value.replace('\n', '\n\t')))
-                fp.write("%s\n" % (key))
-            fp.write("\n")
+                    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):
-        """
-        Private set method
-        """
+        """Set method."""
         if not self.has_section(section):
-            self.add_section( section )
+            self.add_section(section)
+
         if not is_text_string(value):
-            value = repr( value )
+            value = repr(value)
+
         if verbose:
-            print('%s[ %s ] = %s' % (section, option, value))  # spyder: test-skip
-        cp.ConfigParser.set(self, section, option, value)
+            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
-        """
-        # See spyder-ide/spyder#1086 and spyder-ide/spyder#1242 for background
-        # on why this method contains all the exception handling.
-        fname = self.filename()
+        """Save config into the associated .ini file."""
+        fpath = self.get_config_fpath()
 
-        def _write_file(fname):
-            if PY2:
-                # Python 2
-                with codecs.open(fname, 'w', encoding='utf-8') as configfile:
+        def _write_file(fpath):
+            with io.open(fpath, 'w', encoding='utf-8') as configfile:
+                if PY2:
                     self._write(configfile)
-            else:
-                # Python 3
-                with open(fname, 'w', encoding='utf-8') as configfile:
+                else:
                     self.write(configfile)
 
-        try:  # the "easy" way
-            _write_file(fname)
+        # 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(fname):
-                    os.remove(fname)
+            try:
+                # The "delete and sleep" way
+                if osp.isfile(fpath):
+                    os.remove(fpath)
+
                 time.sleep(0.05)
-                _write_file(fname)
+                _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('Failed to write user configuration file to disk, with '
+                      'the exception shown below')  # spyder: test-skip
                 print(e)  # spyder: test-skip
 
-    def filename(self):
-        """Defines the name of the configuration file to use."""
-        # Needs to be done this way to be used by the project config.
-        # To fix on a later PR
-        self._filename = getattr(self, '_filename', None)
-        self._root_path = getattr(self, '_root_path', None)
-
-        if self._filename is None and self._root_path is None:
-            return self._filename_global()
-        else:
-            return self._filename_projects()
-
-    def _filename_projects(self):
-        """Create a .ini filename located in the current project directory.
-        This .ini files stores the specific project preferences for each
-        project created with spyder.
-        """
-        return osp.join(self._root_path, self._filename)
-
-    def _filename_global(self):
-        """Create a .ini filename located in user home directory.
-        This .ini files stores the global spyder preferences.
-        """
-        if self.subfolder is None:
-            config_file = osp.join(get_home_dir(), '.%s.ini' % self.name)
-            return config_file
-        else:
-            folder = get_conf_path()
-            # Save defaults in a "defaults" dir of .spyder2 to not pollute it
-            if 'defaults' in self.name:
-                folder = osp.join(folder, 'defaults')
-                if not osp.isdir(folder):
-                    os.mkdir(folder)
-            config_file = osp.join(folder, '%s.ini' % self.name)
-            return config_file
+    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, False)
+                new_value = options[option]
+                self._set(section, option, new_value, verbose=False)
 
 
-#==============================================================================
+# ============================================================================
 # User config class
-#==============================================================================
+# ============================================================================
 class UserConfig(DefaultsConfig):
     """
-    UserConfig class, based on ConfigParser
-    name: name of the config
-    defaults: dictionnary containing options
-              *or* list of tuples (section_name, options)
-    version: version of the configuration file (X.Y.Z format)
-    subfolder: configuration file will be saved in %home%/subfolder/%name%.ini
-
-    Note that 'get' and 'set' arguments number and type
-    differ from the overriden methods
+    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, {}),]
+        dictionnary containing options *or* list of tuples (sec_name, options)
+    load: bool
+        TODO:
+    version: str
+        version of the configuration file in 'major.minor.micro' format.
+    backup: bool
+        TODO:
+    raw_mode: bool
+        TODO:
+    remove_obsolete: bool
+        TODO:
+
+    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, defaults=None, load=True, version=None,
-                 subfolder=None, backup=False, raw_mode=False,
-                 remove_obsolete=False):
-        DefaultsConfig.__init__(self, name, subfolder)
-        self.raw = 1 if raw_mode else 0
-        if (version is not None and
-                re.match(r'^(\d+).(\d+).(\d+)$', version) is None):
-            raise ValueError("Version number %r is incorrect - must be in X.Y.Z format" % version)
-        if isinstance(defaults, dict):
-            defaults = [ (self.DEFAULT_SECTION_NAME, defaults) ]
-        self.defaults = defaults
-        if defaults is not None:
-            self.reset_to_defaults(save=False)
-        fname = self.filename()
-        if backup:
-            try:
-                shutil.copyfile(fname, "%s.bak" % fname)
-            except IOError:
-                pass
+
+    def __init__(self, name, path, defaults=None, load=True, version=None,
+                 backup=False, raw_mode=False, remove_obsolete=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._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 load:
-            # If config file already exists, it overrides Default options:
-            self.load_from_ini()
-            old_ver = self.get_version(version)
-            _major = lambda _t: _t[:_t.find('.')]
-            _minor = lambda _t: _t[:_t.rfind('.')]
+            # 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(defaults, version, subfolder)
+            self._save_new_defaults(defaults)
+
             # Updating defaults only if major/minor version is different
-            if _minor(version) != _minor(old_ver):
+            if (self._get_minor_version(version=version)
+                    != self._get_minor_version(version=old_version)):
+
                 if backup:
-                    try:
-                        shutil.copyfile(fname, "%s-%s.bak" % (fname, old_ver))
-                    except IOError:
-                        pass
-                if check_version(old_ver, '2.4.0', '<'):
-                    self.reset_to_defaults(save=False)
-                else:
-                    self._update_defaults(defaults, old_ver)
-                if check_version(old_ver, '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)
+                    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 or _major(version) != _major(old_ver):
-                    self._remove_deprecated_options(old_ver)
+                # TODO: This might be problematic for the multi file config
+                # This needs to be called from the multiconfig manager
+                # another option should be added on init for this
+                # if (remove_obsolete or self._get_major_version(version)
+                #         != self._get_major_version(old_version)):
path: spyder/config/user.py
text: @user1:
Why is this commented? Please fix it for this PR.

@author:
Fixed!

diff_hunk: @@ -447,14 +584,376 @@ def set(self, section, option, value, verbose=False, save=True):
             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):
-        cp.ConfigParser.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):
-        cp.ConfigParser.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())
+
+
+class SpyderUserConfig(UserConfig):
+
+    def get_previous_config_fpath(self):
+        """
+        Override method.
+
+        Return the last configuration file used if found.
+        """
+        fpath = self.get_config_fpath()
+        previous_paths = [
+            # >= 52.0.0
+            fpath,
+            # < 52.0.0
+            os.path.join(get_conf_path(), 'spyder.ini'),
+            # < ??
+        ]
+        for fpath in previous_paths:
+            print('fpath', fpath)
+            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.
+        """
+        fpath = self.get_config_fpath()
+
+        if version is None:
+            fpath = self.get_config_fpath()
+        elif check_version(version, '52.0.0', '<'):
+            fpath = osp.join(get_conf_path(), 'spyder.ini')
+        elif check_version(version, '2.4.0', '<'):
+            # TODO: Should this still be supported?
+            pass
+
+        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, '52.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
+
+    # TODO:
path: spyder/config/user.py
text: @user1:
Why this TODO?

@author:
Forgot to remove it after I renamed the method

diff_hunk: @@ -447,14 +584,376 @@ def set(self, section, option, value, verbose=False, save=True):
             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):
-        cp.ConfigParser.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):
-        cp.ConfigParser.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())
+
+
+class SpyderUserConfig(UserConfig):
+
+    def get_previous_config_fpath(self):
+        """
+        Override method.
+
+        Return the last configuration file used if found.
+        """
+        fpath = self.get_config_fpath()
+        previous_paths = [
+            # >= 52.0.0
+            fpath,
+            # < 52.0.0
+            os.path.join(get_conf_path(), 'spyder.ini'),
+            # < ??
+        ]
+        for fpath in previous_paths:
+            print('fpath', fpath)
+            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.
+        """
+        fpath = self.get_config_fpath()
+
+        if version is None:
+            fpath = self.get_config_fpath()
+        elif check_version(version, '52.0.0', '<'):
+            fpath = osp.join(get_conf_path(), 'spyder.ini')
+        elif check_version(version, '2.4.0', '<'):
+            # TODO: Should this still be supported?
+            pass
+
+        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, '52.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
+
+    # TODO:
+    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, '3.0.0', '<='):
+                name = '{}-{}'.format('defaults', old_version)
+                path = self._module_source_path
+            elif check_version(old_version, '52.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()
+
+        return path, name
+
+    def apply_configuration_patches(self, old_version=None):
+        """
+        Override method.
+
+        Apply any patch to configuration values on version changes.
+        """
+        # TODO: is this check still needed?
path: spyder/config/user.py
text: @user1:
Yes, we can leave it without problems. Please remove this TODO.

diff_hunk: @@ -447,14 +584,376 @@ def set(self, section, option, value, verbose=False, save=True):
             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):
-        cp.ConfigParser.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):
-        cp.ConfigParser.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())
+
+
+class SpyderUserConfig(UserConfig):
+
+    def get_previous_config_fpath(self):
+        """
+        Override method.
+
+        Return the last configuration file used if found.
+        """
+        fpath = self.get_config_fpath()
+        previous_paths = [
+            # >= 52.0.0
+            fpath,
+            # < 52.0.0
+            os.path.join(get_conf_path(), 'spyder.ini'),
+            # < ??
+        ]
+        for fpath in previous_paths:
+            print('fpath', fpath)
+            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.
+        """
+        fpath = self.get_config_fpath()
+
+        if version is None:
+            fpath = self.get_config_fpath()
+        elif check_version(version, '52.0.0', '<'):
+            fpath = osp.join(get_conf_path(), 'spyder.ini')
+        elif check_version(version, '2.4.0', '<'):
+            # TODO: Should this still be supported?
+            pass
+
+        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, '52.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
+
+    # TODO:
+    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, '3.0.0', '<='):
+                name = '{}-{}'.format('defaults', old_version)
+                path = self._module_source_path
+            elif check_version(old_version, '52.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()
+
+        return path, name
+
+    def apply_configuration_patches(self, old_version=None):
+        """
+        Override method.
+
+        Apply any patch to configuration values on version changes.
+        """
+        # TODO: is this check still needed?
+        if old_version and check_version(old_version, '2.4.0', '<'):
+            self.reset_to_defaults(save=False)
+        else:
+            # TODO: Remove comment if tests pass ok
+            # self._update_defaults(defaults, old_version)
path: spyder/config/user.py
text: @user1:
Why this TODO?

@author:
Forgot to remove it

diff_hunk: @@ -447,14 +584,376 @@ def set(self, section, option, value, verbose=False, save=True):
             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):
-        cp.ConfigParser.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):
-        cp.ConfigParser.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())
+
+
+class SpyderUserConfig(UserConfig):
+
+    def get_previous_config_fpath(self):
+        """
+        Override method.
+
+        Return the last configuration file used if found.
+        """
+        fpath = self.get_config_fpath()
+        previous_paths = [
+            # >= 52.0.0
+            fpath,
+            # < 52.0.0
+            os.path.join(get_conf_path(), 'spyder.ini'),
+            # < ??
+        ]
+        for fpath in previous_paths:
+            print('fpath', fpath)
path: spyder/config/user.py
text: @user1:
Tests are failing due to this print.

diff_hunk: @@ -447,14 +584,376 @@ def set(self, section, option, value, verbose=False, save=True):
             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):
-        cp.ConfigParser.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):
-        cp.ConfigParser.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())
+
+
+class SpyderUserConfig(UserConfig):
+
+    def get_previous_config_fpath(self):
+        """
+        Override method.
+
+        Return the last configuration file used if found.
+        """
+        fpath = self.get_config_fpath()
+        previous_paths = [
+            # >= 52.0.0
+            fpath,
+            # < 52.0.0
+            os.path.join(get_conf_path(), 'spyder.ini'),
+            # < ??
+        ]
+        for fpath in previous_paths:
+            print('fpath', fpath)
+            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.
+        """
+        fpath = self.get_config_fpath()
+
+        if version is None:
+            fpath = self.get_config_fpath()
+        elif check_version(version, '52.0.0', '<'):
+            fpath = osp.join(get_conf_path(), 'spyder.ini')
+        elif check_version(version, '2.4.0', '<'):
+            # TODO: Should this still be supported?
+            pass
+
+        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, '52.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
+
+    # TODO:
+    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, '3.0.0', '<='):
+                name = '{}-{}'.format('defaults', old_version)
+                path = self._module_source_path
+            elif check_version(old_version, '52.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()
+
+        return path, name
+
+    def apply_configuration_patches(self, old_version=None):
+        """
+        Override method.
+
+        Apply any patch to configuration values on version changes.
+        """
+        # TODO: is this check still needed?
+        if old_version and check_version(old_version, '2.4.0', '<'):
+            self.reset_to_defaults(save=False)
+        else:
+            # TODO: Remove comment if tests pass ok
+            # self._update_defaults(defaults, old_version)
+            self._update_defaults(self.defaults, old_version)
+
+        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):
+    """
+    Multi user config class based emulating the basic interface to UserConfig.
+
+    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):
+        """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._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': remove_obsolete,
+        }
+        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)
+
+    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."""
+        pass
path: spyder/config/user.py
text: @user1:
Why is there a pass here and a return below?

@author:
This is a check for the name map to make sure  section/options are not repeated on several files. (updating now)

diff_hunk: @@ -447,14 +584,376 @@ def set(self, section, option, value, verbose=False, save=True):
             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):
-        cp.ConfigParser.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):
-        cp.ConfigParser.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())
+
+
+class SpyderUserConfig(UserConfig):
+
+    def get_previous_config_fpath(self):
+        """
+        Override method.
+
+        Return the last configuration file used if found.
+        """
+        fpath = self.get_config_fpath()
+        previous_paths = [
+            # >= 52.0.0
+            fpath,
+            # < 52.0.0
+            os.path.join(get_conf_path(), 'spyder.ini'),
+            # < ??
+        ]
+        for fpath in previous_paths:
+            print('fpath', fpath)
+            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.
+        """
+        fpath = self.get_config_fpath()
+
+        if version is None:
+            fpath = self.get_config_fpath()
+        elif check_version(version, '52.0.0', '<'):
+            fpath = osp.join(get_conf_path(), 'spyder.ini')
+        elif check_version(version, '2.4.0', '<'):
+            # TODO: Should this still be supported?
+            pass
+
+        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, '52.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
+
+    # TODO:
+    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, '3.0.0', '<='):
+                name = '{}-{}'.format('defaults', old_version)
+                path = self._module_source_path
+            elif check_version(old_version, '52.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()
+
+        return path, name
+
+    def apply_configuration_patches(self, old_version=None):
+        """
+        Override method.
+
+        Apply any patch to configuration values on version changes.
+        """
+        # TODO: is this check still needed?
+        if old_version and check_version(old_version, '2.4.0', '<'):
+            self.reset_to_defaults(save=False)
+        else:
+            # TODO: Remove comment if tests pass ok
+            # self._update_defaults(defaults, old_version)
+            self._update_defaults(self.defaults, old_version)
+
+        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):
+    """
+    Multi user config class based emulating the basic interface to UserConfig.
path: spyder/config/user.py
text: @user1:
Please change this to

    Multiuser config class which emulates the basic UserConfig interface.

@author:
Done

diff_hunk: @@ -447,14 +584,376 @@ def set(self, section, option, value, verbose=False, save=True):
             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):
-        cp.ConfigParser.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):
-        cp.ConfigParser.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())
+
+
+class SpyderUserConfig(UserConfig):
+
+    def get_previous_config_fpath(self):
+        """
+        Override method.
+
+        Return the last configuration file used if found.
+        """
+        fpath = self.get_config_fpath()
+        previous_paths = [
+            # >= 52.0.0
+            fpath,
+            # < 52.0.0
+            os.path.join(get_conf_path(), 'spyder.ini'),
+            # < ??
path: spyder/config/user.py
text: @user1:
Preferences were saved in `~/.spyder{-py3}` on Linux before Spyder 3, but I think it's not necessary to cover that case here. So please remove this comment.

@author:
Ok

diff_hunk: @@ -0,0 +1,168 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+Configuration manager providing access to user/site/project configuration.
+"""
+
+# Standard library imports
+import os
+import os.path as osp
+
+# Local imports
+from spyder.config.base import get_conf_path, get_conf_subfolder, get_home_dir
+from spyder.config.main import CONF_VERSION, DEFAULTS, NAME_MAP
+from spyder.config.user import MultiUserConfig, NoDefault
+
+
+class ConfigurationManager(object):
+    """
+    Configuration manager to provide access to user/site/project config.
+    """
+
+    def __init__(self, parent=None, active_project_callback=None):
+        """
+        Configuration manager to provide access to user/site/project config.
+        """
+        path = self.get_user_config_path()
+        if not osp.isdir(path):
+            os.makedirs(path)
+
+        self._parent = parent
+        self._active_project_callback = active_project_callback
+        self._user_config = MultiUserConfig(
+            NAME_MAP,
+            path=path,
+            defaults=DEFAULTS,
+            load=True,
+            version=CONF_VERSION,
+            backup=True,
+            raw_mode=True,
+            remove_obsolete=False,
+        )
+        # TODO: Implementation to be defined
+        self._site_config = None
+
+        # TODO: To be implemented in the following PR
+        self._project_configs = {}  # Cache project configurations
+
+        # Setup
+        self.remove_deprecated_config_locations()
+
+    def remove_deprecated_config_locations(self):
+        """Removing old .spyder.ini location."""
+        old_location = osp.join(get_home_dir(), '.spyder.ini')
+        if osp.isfile(old_location):
+            os.remove(old_location)
+
+    def get_system_config_path(self):
+        """Return the system configuration path."""
+        return None
+
+    def get_active_conf(self):
+        """
+        Return the active project configuration or the user configurarion.
+        """
+        # TODO: implement project configuration on the following PR
+        return self._user_config
+
+    def get_user_config_path(self):
+        """Return the user configuration path."""
+        base_path = get_conf_path()
+        path = osp.join(base_path, 'app', 'config')
path: spyder/config/manager.py
text: @user1:
Why is `app` needed here?

@author:
The configuration folder is now like this to isolate application "stuff" and plugins "stuff"

so

```
# Spyder application
~/.spyder/app/config/...
~/.spyder/app/<other files to move here relevant to the application and not plugins>

# Spyder plugins
~/.spyder/plugins/<plugin-name>/config/...
~/.spyder/plugins/<plugin-name>/<other files relevant to the plugin>
```

I believe this way we can move some of the other files inside `.spyder` to a more convenient place so that it is less messy?. Thoughts @user1 ?