Repository: spyder-ide/spyder

Issue:
Disabling the Projects pluging crashes spyder
<!--- **PLEASE READ:** When submitting here, please ensure you've completed the following checklist and checked the boxes to confirm. Issue reports without it may be closed. Thanks! --->

### Issue Report Checklist

* [x] Searched the [issues page](https://github.com/spyder-ide/spyder/issues?q=is%3Aissue) for similar reports
* [x] Read the relevant sections of the [Spyder Troubleshooting Guide](https://github.com/spyder-ide/spyder/wiki/Troubleshooting-Guide-and-FAQ) and followed its advice
* [x] Reproduced the issue after updating with ``conda update spyder`` (or ``pip``, if not using Anaconda)
* [ ] Could not reproduce inside ``jupyter qtconsole`` (if console-related)
* [x] Tried basic troubleshooting (if a bug/error)
    * [x] Restarted Spyder
    * [x] Reset preferences with ``spyder --reset``
    * [ ] Reinstalled the latest version of [Anaconda](https://www.anaconda.com/download/)
    * [ ] Tried the other applicable steps from the Troubleshooting Guide
* [ ] Completed the **Problem Description**, **Steps to Reproduce** and **Version** sections below


## Problem Description

Disabling the Projects pluging crashes spyder when trying to open a new file.

### What steps reproduce the problem?

1. spyder --reset && spyder
2. Disable the Project plugin and restart
3. Try to create a new file (File > New File)
4. spyder crashes

### What is the expected output? What do you see instead?

Spyder should not crash :)

### Paste Traceback/Error Below (if applicable)
<!--- Copy from error dialog or View > Panes > Internal Console --->

```python-traceback
Traceback (most recent call last):
  File "/Users/remisalmon/Miniconda/envs/spyder/lib/python3.8/site-packages/spyder/plugins/editor/plugin.py", line 2017, in new
    if self.main.projects.get_active_project() is not None:
AttributeError: 'NoneType' object has no attribute 'get_active_project'
```

## Versions
<!--- You can get this information from Help > About Spyder...
or (if Spyder won't launch) the "conda list" command
from the Anaconda Prompt/Terminal/command line. --->

* Spyder version: 5.2.0 None
* Python version: 3.8.12 64-bit
* Qt version: 5.12.9
* PyQt5 version: 5.12.3
* Operating System: Darwin 20.6.0

### Dependencies
<!--- Please go to the menu entry Help > Dependencies,
press the Copy to clipboard button and paste below --->

```

# Mandatory:
applaunchservices >=0.1.7     :  0.2.1 (OK)
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.3 (OK)
diff_match_patch >=20181111   :  20200713 (OK)
intervaltree >=3.0.2          :  3.0.2 (OK)
IPython >=7.6.0               :  7.30.0 (OK)
jedi >=0.17.2;<0.19.0         :  0.18.1 (OK)
jellyfish >=0.7               :  0.8.9 (OK)
jsonschema >=3.2.0            :  4.2.1 (OK)
keyring >=17.0.0              :  23.4.0 (OK)
nbconvert >=4.0               :  6.3.0 (OK)
numpydoc >=0.6.0              :  1.1.0 (OK)
parso >=0.7.0;<0.9.0          :  0.8.3 (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.12.1 (OK)
pyls_spyder >=0.4.0           :  0.4.0 (OK)
pylsp >=1.3.1;<1.4.0          :  1.3.2 (OK)
pylsp_black >=1.0.0           :  None (OK)
qdarkstyle =3.0.2             :  3.0.2 (OK)
qstylizer >=0.1.10            :  0.2.1 (OK)
qtawesome >=1.0.2             :  1.1.1 (OK)
qtconsole >=5.2.1;<5.3.0      :  5.2.1 (OK)
qtpy >=1.5.0                  :  1.11.2 (OK)
rtree >=0.9.7                 :  0.9.7 (OK)
setuptools >=49.6.0           :  59.4.0 (OK)
sphinx >=0.6.6                :  4.3.1 (OK)
spyder_kernels >=2.2.0;<2.3.0 :  2.2.0 (OK)
textdistance >=4.2.0          :  4.2.2 (OK)
three_merge >=0.1.1           :  0.1.1 (OK)
watchdog >=0.10.3             :  2.1.6 (OK)
zmq >=17                      :  22.3.0 (OK)

# Optional:
cython >=0.21                 :  None (OK)
matplotlib >=2.0.0            :  3.5.0 (OK)
numpy >=1.7                   :  1.21.4 (OK)
pandas >=1.1.1                :  1.3.4 (OK)
scipy >=0.17.0                :  None (OK)
sympy >=0.7.3                 :  None (OK)
```



Discussion:
Hey @remisalmon, thanks for reporting. We'll fix this problem in our next version (5.2.1), to be released shortly.

Resolving patch:
diff --git a/spyder/plugins/editor/plugin.py b/spyder/plugins/editor/plugin.py
--- a/spyder/plugins/editor/plugin.py
+++ b/spyder/plugins/editor/plugin.py
@@ -385,7 +385,9 @@ def register_completion_capabilities(self, capabilities, language):
         # This is required to start workspace before completion
         # services when Spyder starts with an open project.
         # TODO: Find a better solution for it in the future!!
-        self.main.projects.start_workspace_services()
+        projects = self.main.get_plugin(Plugins.Projects, error=False)
+        if projects:
+            projects.start_workspace_services()
 
         self.completion_capabilities[language] = dict(capabilities)
         for editorstack in self.editorstacks:
@@ -1884,7 +1886,6 @@ def update_active_languages(self, languages):
         if self.main.get_plugin(Plugins.Completions, error=False):
             self.main.completions.update_client_status(languages)
 
-
     # ------ Bookmarks
     def save_bookmarks(self, filename, bookmarks):
         """Receive bookmark changes and save them."""
@@ -2017,8 +2018,9 @@ def new(self, fname=None, editorstack=None, text=None):
                     break
             basedir = getcwd_or_home()
 
-            if self.main.projects.get_active_project() is not None:
-                basedir = self.main.projects.get_active_project_path()
+            projects = self.main.get_plugin(Plugins.Projects, error=False)
+            if projects and projects.get_active_project() is not None:
+                basedir = projects.get_active_project_path()
             else:
                 c_fname = self.get_current_filename()
                 if c_fname is not None and c_fname != self.TEMPFILE_PATH:
@@ -2249,12 +2251,16 @@ def _convert(fname):
             else:
                 # processevents is false only when calling from debugging
                 current_editor.sig_debug_stop.emit(goto[index])
-                current_sw = self.main.ipyconsole.get_current_shellwidget()
-                current_sw.sig_prompt_ready.connect(
-                    current_editor.sig_debug_stop[()].emit)
-                current_pdb_state = self.main.ipyconsole.get_pdb_state()
-                pdb_last_step = self.main.ipyconsole.get_pdb_last_step()
-                self.update_pdb_state(current_pdb_state, pdb_last_step)
+
+                ipyconsole = self.main.get_plugin(
+                    Plugins.IPythonConsole, error=False)
+                if ipyconsole:
+                    current_sw = ipyconsole.get_current_shellwidget()
+                    current_sw.sig_prompt_ready.connect(
+                        current_editor.sig_debug_stop[()].emit)
+                    current_pdb_state = ipyconsole.get_pdb_state()
+                    pdb_last_step = ipyconsole.get_pdb_last_step()
+                    self.update_pdb_state(current_pdb_state, pdb_last_step)
 
     @Slot()
     def print_file(self):
@@ -2296,8 +2302,14 @@ def can_close_file(self, filename=None):
         """
         if not CONF.get('ipython_console', 'pdb_prevent_closing'):
             return True
-        debugging = self.main.ipyconsole.get_pdb_state()
-        last_pdb_step = self.main.ipyconsole.get_pdb_last_step()
+        ipyconsole = self.main.get_plugin(Plugins.IPythonConsole, error=False)
+
+        debugging = False
+        last_pdb_step = {}
+        if ipyconsole:
+            debugging = ipyconsole.get_pdb_state()
+            last_pdb_step = ipyconsole.get_pdb_last_step()
+
         can_close = True
         if debugging and 'fname' in last_pdb_step and filename:
             if osp.normcase(last_pdb_step['fname']) == osp.normcase(filename):
@@ -2581,7 +2593,7 @@ def current_file_changed(self, filename, position, line, column):
             current_stack.hide_tooltip()
 
         # Update debugging state
-        ipyconsole = getattr(self.main, 'ipyconsole', None)
+        ipyconsole = self.main.get_plugin(Plugins.IPythonConsole, error=False)
         if ipyconsole is not None:
             pdb_state = ipyconsole.get_pdb_state()
             pdb_last_step = ipyconsole.get_pdb_last_step()
@@ -2712,13 +2724,17 @@ def clear_breakpoint(self, filename, lineno):
 
     def stop_debugging(self):
         """Stop debugging"""
-        self.main.ipyconsole.stop_debugging()
+        ipyconsole = self.main.get_plugin(Plugins.IPythonConsole, error=False)
+        if ipyconsole:
+            ipyconsole.stop_debugging()
 
     def debug_command(self, command):
         """Debug actions"""
         self.switch_to_plugin()
-        self.main.ipyconsole.pdb_execute_command(command)
-        self.main.ipyconsole.switch_to_plugin()
+        ipyconsole = self.main.get_plugin(Plugins.IPythonConsole, error=False)
+        if ipyconsole:
+            ipyconsole.pdb_execute_command(command)
+            ipyconsole.switch_to_plugin()
 
     # ----- Handlers for the IPython Console kernels
     def _get_editorstack(self):


Test patch:
diff --git a/spyder/plugins/editor/tests/conftest.py b/spyder/plugins/editor/tests/conftest.py
--- a/spyder/plugins/editor/tests/conftest.py
+++ b/spyder/plugins/editor/tests/conftest.py
@@ -11,9 +11,11 @@
 import os.path as osp
 from unittest.mock import MagicMock, Mock
 
+from spyder.api.plugins import Plugins
+from spyder.utils.qthelpers import qapplication
+
 # This is needed to avoid an error because QtAwesome
 # needs a QApplication to work correctly.
-from spyder.utils.qthelpers import qapplication
 app = qapplication()
 
 from qtpy.QtWidgets import QMainWindow
@@ -41,20 +43,18 @@ class MainMock(QMainWindow):
         def __getattr__(self, attr):
             if attr.endswith('actions'):
                 return []
-            elif attr == 'projects':
-                projects = Mock()
-                projects.get_active_project.return_value = None
-                return projects
-            elif attr == 'ipyconsole':
-                ipyconsole = Mock()
-                ipyconsole.get_pdb_state.return_value = False
-                return ipyconsole
             else:
                 return Mock()
 
         def get_spyder_pythonpath(*args):
             return []
 
+        def get_plugin(self, plugin_name, error=True):
+            if plugin_name in [Plugins.IPythonConsole, Plugins.Projects]:
+                return None
+            else:
+                return Mock()
+
     window = MainMock()
     editor = Editor(window)
     window.setCentralWidget(editor)