Repository: spyder-ide/spyder

Issue:
DuplicateOptionError when reading workspace.ini project file
## Description

### What steps will reproduce the problem?

<!--- You can use Markdown here --->

spyder can not open!!!!!!!spyder can not open!!!!!!!spyder can not open!!!!!!!

### Traceback
```python-traceback
Traceback (most recent call last):
  File "C:\ProgramData\Anaconda3\lib\site-packages\spyder\plugins\projects\plugin.py", line 379, in create_new_project
    self._create_project(root_path, project_type_id=project_type)
  File "C:\ProgramData\Anaconda3\lib\site-packages\spyder\plugins\projects\plugin.py", line 388, in _create_project
    project = project_type_class(
  File "C:\ProgramData\Anaconda3\lib\site-packages\spyder\plugins\projects\api.py", line 47, in __init__
    self.config = ProjectMultiConfig(
  File "C:\ProgramData\Anaconda3\lib\site-packages\spyder\config\user.py", line 786, in __init__
    self._configs_map[name] = config_class(**new_kwargs)
  File "C:\ProgramData\Anaconda3\lib\site-packages\spyder\config\user.py", line 212, in __init__
    self._load_from_ini(previous_fpath)
  File "C:\ProgramData\Anaconda3\lib\site-packages\spyder\config\user.py", line 333, in _load_from_ini
    self.read(fpath, encoding='utf-8')
  File "C:\ProgramData\Anaconda3\lib\configparser.py", line 697, in read
    self._read(fp, filename)
  File "C:\ProgramData\Anaconda3\lib\configparser.py", line 1096, in _read
    raise DuplicateOptionError(sectname, optname,
configparser.DuplicateOptionError: While reading from 'D:\\SVN\\2022_NR L1 vRan\\2022_vRAN_L1_Analysis_Tool_Dev\\1.0 Engineering\\3.0 实现\\DevBranch\\NrL1LogAnalyzeTool\\.spyproject\\config\\workspace.ini' [line 10]: option 'recent_files' in section 'workspace' already exists
```

## Versions

* Spyder version: 5.1.5 
* Python version: 3.9.7
* Qt version: 5.9.7
* PyQt5 version: 5.9.2
* Operating System: Windows 10

### Dependencies

```

# Mandatory:
atomicwrites >=1.2.0          :  1.4.0 (OK)
chardet >=2.0.0               :  4.0.0 (OK)
cloudpickle >=0.5.0           :  2.0.0 (OK)
cookiecutter >=1.6.0          :  1.7.2 (OK)
diff_match_patch >=20181111   :  20200713 (OK)
intervaltree >=3.0.2          :  3.1.0 (OK)
IPython >=7.6.0               :  7.29.0 (OK)
jedi >=0.17.2;<0.19.0         :  0.18.0 (OK)
jsonschema >=3.2.0            :  3.2.0 (OK)
keyring >=17.0.0              :  23.1.0 (OK)
nbconvert >=4.0               :  6.1.0 (OK)
numpydoc >=0.6.0              :  1.1.0 (OK)
paramiko >=2.4.0              :  2.7.2 (OK)
parso >=0.7.0;<0.9.0          :  0.8.2 (OK)
pexpect >=4.4.0               :  4.8.0 (OK)
pickleshare >=0.4             :  0.7.5 (OK)
psutil >=5.3                  :  5.8.0 (OK)
pygments >=2.0                :  2.10.0 (OK)
pylint >=2.5.0;<2.10.0        :  2.9.6 (OK)
pyls_spyder >=0.4.0           :  0.4.0 (OK)
pylsp >=1.2.2;<1.3.0          :  1.2.4 (OK)
pylsp_black >=1.0.0           :  None (OK)
qdarkstyle =3.0.2             :  3.0.2 (OK)
qstylizer >=0.1.10            :  0.1.10 (OK)
qtawesome >=1.0.2             :  1.0.2 (OK)
qtconsole >=5.1.0             :  5.1.1 (OK)
qtpy >=1.5.0                  :  1.10.0 (OK)
rtree >=0.9.7                 :  0.9.7 (OK)
setuptools >=49.6.0           :  58.0.4 (OK)
sphinx >=0.6.6                :  4.2.0 (OK)
spyder_kernels >=2.1.1;<2.2.0 :  2.1.3 (OK)
textdistance >=4.2.0          :  4.2.1 (OK)
three_merge >=0.1.1           :  0.1.1 (OK)
watchdog >=0.10.3             :  2.1.3 (OK)
zmq >=17                      :  22.2.1 (OK)

# Optional:
cython >=0.21                 :  0.29.24 (OK)
matplotlib >=2.0.0            :  3.4.3 (OK)
numpy >=1.7                   :  1.20.3 (OK)
pandas >=1.1.1                :  1.3.4 (OK)
scipy >=0.17.0                :  1.7.1 (OK)
sympy >=0.7.3                 :  1.9 (OK)
```



Discussion:
Hey @Gcxchunxia, thanks for reporting. Please remove this file for Spyder to work again:

    D:\\SVN\\2022_NR L1 vRan\\2022_vRAN_L1_Analysis_Tool_Dev\\1.0 Engineering\\3.0 实现\\DevBranch\\NrL1LogAnalyzeTool\\.spyproject\\config\\workspace.ini

We'll make Spyder not to crash for errors like this in a future release.

Resolving patch:
diff --git a/spyder/plugins/projects/api.py b/spyder/plugins/projects/api.py
--- a/spyder/plugins/projects/api.py
+++ b/spyder/plugins/projects/api.py
@@ -9,8 +9,10 @@
 """
 
 # Standard library imports
-import os.path as osp
 from collections import OrderedDict
+import configparser
+import os.path as osp
+import shutil
 
 # Local imports
 from spyder.api.translations import get_translation
@@ -39,17 +41,20 @@ def __init__(self, root_path, parent_plugin=None):
         self.root_path = root_path
         self.open_project_files = []
         self.open_non_project_files = []
+
+        # Path to project's config directory
         path = osp.join(root_path, get_project_config_folder(), 'config')
-        self.config = ProjectMultiConfig(
-            PROJECT_NAME_MAP,
-            path=path,
-            defaults=PROJECT_DEFAULTS,
-            load=True,
-            version=PROJECT_CONF_VERSION,
-            backup=True,
-            raw_mode=True,
-            remove_obsolete=False,
-        )
+
+        # This is necessary to avoid any issues while reading the project's
+        # config files.
+        # Fixes spyder-ide/spyder#17907
+        try:
+            self.config = self.create_config(path)
+        except configparser.Error:
+            # Remove config directory in case of errors and recreate it again.
+            shutil.rmtree(path)
+            self.config = self.create_config(path)
+
         act_name = self.get_option("project_type")
         if not act_name:
             self.set_option("project_type", self.ID)
@@ -100,6 +105,20 @@ def get_recent_files(self):
 
         return list(OrderedDict.fromkeys(recent_files))
 
+    @staticmethod
+    def create_config(path):
+        """Create the project's configuration object."""
+        return ProjectMultiConfig(
+            PROJECT_NAME_MAP,
+            path=path,
+            defaults=PROJECT_DEFAULTS,
+            load=True,
+            version=PROJECT_CONF_VERSION,
+            backup=True,
+            raw_mode=True,
+            remove_obsolete=False,
+        )
+
     # --- API
     # ------------------------------------------------------------------------
     @staticmethod
diff --git a/spyder/plugins/projects/plugin.py b/spyder/plugins/projects/plugin.py
--- a/spyder/plugins/projects/plugin.py
+++ b/spyder/plugins/projects/plugin.py
@@ -433,6 +433,7 @@ def create_new_project(self):
         project_type = data.get("project_type", EmptyProject.ID)
 
         if result:
+            logger.debug(f'Creating a project at {root_path}')
             self._create_project(root_path, project_type_id=project_type)
             dlg.close()
 
@@ -937,11 +938,11 @@ def _load_project_type_class(self, path):
 
             # This is necessary to catch an error for projects created in
             # Spyder 4 or older versions.
-            # Fixes spyder-ide/spyder17097
+            # Fixes spyder-ide/spyder#17097
             try:
                 project_type_id = config[WORKSPACE].get(
                     "project_type", EmptyProject.ID)
-            except KeyError:
+            except Exception:
                 pass
 
         EmptyProject._PARENT_PLUGIN = self


Test patch:
diff --git a/spyder/plugins/projects/tests/test_plugin.py b/spyder/plugins/projects/tests/test_plugin.py
--- a/spyder/plugins/projects/tests/test_plugin.py
+++ b/spyder/plugins/projects/tests/test_plugin.py
@@ -11,11 +11,12 @@
 """
 
 # Standard library imports
+import configparser
 import os
-import shutil
 import os.path as osp
+import shutil
 import sys
-from unittest.mock import Mock, MagicMock
+from unittest.mock import MagicMock
 
 # Third party imports
 import pytest
@@ -26,6 +27,7 @@
 from spyder.config.base import running_in_ci
 from spyder.config.manager import CONF
 import spyder.plugins.base
+from spyder.plugins.projects.api import BaseProjectType
 from spyder.plugins.projects.plugin import Projects, QMessageBox
 from spyder.plugins.preferences.tests.conftest import MainWindowMock
 from spyder.plugins.projects.widgets.projectdialog import ProjectDialog
@@ -298,8 +300,6 @@ def test_recent_projects_menu_action(projects, tmpdir):
 
     Regression test for spyder-ide/spyder#8450.
     """
-    recent_projects_len = len(projects.recent_projects)
-
     # Create the directories.
     path0 = to_text_string(tmpdir.mkdir('project0'))
     path1 = to_text_string(tmpdir.mkdir('project1'))
@@ -515,5 +515,50 @@ def test_reopen_project(projects, tmpdir):
     projects.close_project()
 
 
+def test_recreate_project_config(projects, tmpdir):
+    """
+    Test that the project's config files are recreated when there are errors
+    reading them.
+
+    Regression test for spyder-ide/spyder#17907.
+    """
+    # Create a new directory
+    path = str(tmpdir.mkdir('error_reading_config'))
+
+    # Open project in path to generate its config
+    projects.open_project(path=path)
+
+    # Get project's config path
+    config_path = projects.current_active_project.config._path
+
+    # Close project
+    projects.close_project()
+
+    # Append the contents of a config file to it in order to give an error
+    # while reading it
+    config_file = osp.join(config_path, 'workspace.ini')
+
+    with open(config_file, 'r') as f:
+        file_contents = f.readlines()
+
+    with open(config_file, 'a') as f:
+        for line in file_contents:
+            f.write(line)
+
+    # Try to read config and check we get an error
+    with pytest.raises(configparser.Error):
+        BaseProjectType.create_config(config_path)
+
+    # Reopen the project again and check we recreated the config file we
+    # changed
+    projects.open_project(path=path)
+    projects.close_project()
+
+    with open(config_file, 'r') as f:
+        new_file_contents = f.readlines()
+
+    assert file_contents == new_file_contents
+
+
 if __name__ == "__main__":
     pytest.main()