Repository: spyder-ide/spyder

Issue:
Removing a project deletes also the files on the disk
When removing a Spyder project within the project explorer, not only is the project deleted but also all files contained in the HDD without any warning about that. This is surely not the behavior one expects, as in Spyder 2 the deletion of files had to be explicitly selected. Other project/versioning software usually also has the Spyder 2 behavior regarding project deletion.

**What steps will reproduce the problem?**
1. Create a project, either new or from an existing directory
2. Delete the project

**What is the expected output? What do you see instead?**
The **Project** created by Spyder should be removed. But not all the other files contained in the project-directory.

**Please provide any additional information below**
I understand that this case may be wanted sometimes. But it surely should not be the default behavior. What would otherwise be the proper way to just delete a project (NOT the files)?
My proposal would be to either:
- change the behavior of the deletion function to only delete the Spyder-project or
- **explicitly** state that this will also delete ALL the files **from the disk**

## Versions and main components
- Spyder Version: 3.0.1
- Python Version: 2.7
- Operating system: Ubuntu 16.04 64x
## Dependencies

I think deps don't matter here

Best,
Mayou



Discussion:
Thanks for reporting. We simply didn't consider the use case you reported.

---

@rlaverde, please work on this one. The idea is to change the behavior of the _Delete_ action from the context menu of the Project Explorer when users do a right click on the project directory (i.e. the first row in the explorer).

In this case, _Delete_ must
1. Remove the underlying `.spyproject` directory
2. Close the project and return to the interface without a project.
3. Add a warning to the dialog shown when _Delete_ is selected, saying that the project will be deleted but that the files are going to be preserved on disk.

Please also make all of this work when users press the _Del_ keystroke.

@ccordoba12 should we ask if the files want to be deleted as well ? or not

I'm not sure, but it sounds like a good idea (although it shouldn't be the default one).

However, to make things more explicit (and not only dependent on the context menu entry), I think we should add a _Delete Project_ entry to our _Projects_ menu. What do you think?

> I'm not sure, but it sounds like a good idea (although it shouldn't be the default one).

Agreed lets ask but default should be No, so that accidental enters would also yield no.

> However, to make things more explicit (and not only dependent on the context menu entry), I think we should add a Delete Project entry to our Projects menu. What do you think?

:+1:

@dalthviz, please work on this one instead. Given that issue #3616 seems to be not much of a problem, and you're around, please work on it :-)


Resolving patch:
diff --git a/spyder/plugins/projects.py b/spyder/plugins/projects.py
--- a/spyder/plugins/projects.py
+++ b/spyder/plugins/projects.py
@@ -93,6 +93,9 @@ def get_plugin_actions(self):
         self.close_project_action = create_action(self,
                                     _("Close Project"),
                                     triggered=self.close_project)
+        self.delete_project_action = create_action(self,
+                                    _("Delete Project"),
+                                    triggered=self.delete_project)
         self.clear_recent_projects_action =\
             create_action(self, _("Clear this list"),
                           triggered=self.clear_recent_projects)
@@ -106,6 +109,7 @@ def get_plugin_actions(self):
                                             None,
                                             self.open_project_action,
                                             self.close_project_action,
+                                            self.delete_project_action,
                                             None,
                                             self.recent_project_menu,
                                             explorer_action]
@@ -190,6 +194,7 @@ def update_project_actions(self):
 
         active = bool(self.get_active_project_path())
         self.close_project_action.setEnabled(active)
+        self.delete_project_action.setEnabled(active)
         self.edit_project_preferences_action.setEnabled(active)
 
     def edit_project_preferences(self):
diff --git a/spyder/widgets/explorer.py b/spyder/widgets/explorer.py
--- a/spyder/widgets/explorer.py
+++ b/spyder/widgets/explorer.py
@@ -142,6 +142,8 @@ def setup_view(self):
         self.sortByColumn(0, Qt.AscendingOrder)
         self.fsmodel.modelReset.connect(self.reset_icon_provider)
         self.reset_icon_provider()
+        # Disable the view of .spyproject. 
+        self.filter_directories()
         
     def set_name_filters(self, name_filters):
         """Set name filters"""
@@ -541,11 +543,21 @@ def delete(self, fnames=None):
         multiple = len(fnames) > 1
         yes_to_all = None
         for fname in fnames:
-            yes_to_all = self.delete_file(fname, multiple, yes_to_all)
-            if yes_to_all is not None and not yes_to_all:
-                # Canceled
-                return
-
+            spyproject_path = osp.join(fname,'.spyproject')
+            if osp.isdir(fname) and osp.exists(spyproject_path):
+                QMessageBox.information(self, _('File Explorer'),
+                                        _("The current directory contains a "
+                                        "project.<br><br>"
+                                        "If you want to delete"
+                                        " the project, please go to "
+                                        "<b>Projects</b> &raquo; <b>Delete "
+                                        "Project</b>"))
+            else:    
+                yes_to_all = self.delete_file(fname, multiple, yes_to_all)
+                if yes_to_all is not None and not yes_to_all:
+                    # Canceled
+                    break
+                
     def convert_notebook(self, fname):
         """Convert an IPython notebook to a Python script in editor"""
         try: 
@@ -814,7 +826,12 @@ def restore_expanded_state(self):
                                                   self.restore_directory_state)
                 self.fsmodel.directoryLoaded.connect(
                                                 self.follow_directories_loaded)
-
+                
+    def filter_directories(self):
+        """Filter the directories to show"""
+        index = self.get_index('.spyproject')
+        if index is not None:
+            self.setRowHidden(index.row(), index.parent(), True)
 
 class ProxyModel(QSortFilterProxyModel):
     """Proxy model: filters tree view"""
@@ -908,8 +925,9 @@ def setup_project_view(self):
         for i in [1, 2, 3]:
             self.hideColumn(i)
         self.setHeaderHidden(True)
-
-
+        # Disable the view of .spyproject. 
+        self.filter_directories()
+      
 class ExplorerTreeWidget(DirView):
     """File/directory explorer tree widget
     show_cd_only: Show current directory only
@@ -987,6 +1005,8 @@ def refresh(self, new_path=None, force_current=False):
                              self.histindex is not None and self.histindex > 0)
         self.set_next_enabled.emit(self.histindex is not None and \
                                    self.histindex < len(self.history)-1)
+        # Disable the view of .spyproject. 
+        self.filter_directories()
             
     #---- Events
     def directory_clicked(self, dirname):
diff --git a/spyder/widgets/projects/explorer.py b/spyder/widgets/projects/explorer.py
--- a/spyder/widgets/projects/explorer.py
+++ b/spyder/widgets/projects/explorer.py
@@ -30,6 +30,8 @@
 
 class ExplorerTreeWidget(FilteredDirView):
     """Explorer tree widget"""
+    
+    sig_delete_project = Signal()
 
     def __init__(self, parent, show_hscrollbar=True):
         FilteredDirView.__init__(self, parent)
@@ -149,6 +151,21 @@ def dropEvent(self, event):
                                        "<br><br>Error message:<br>%s"
                                        ) % (action_str, src,
                                             to_text_string(error)))
+    @Slot()
+    def delete(self, fnames=None):
+        """Delete files"""
+        if fnames is None:
+            fnames = self.get_selected_filenames()
+        multiple = len(fnames) > 1
+        yes_to_all = None
+        for fname in fnames:
+            if fname == self.proxymodel.path_list[0]:
+                self.sig_delete_project.emit()
+            else:
+                yes_to_all = self.delete_file(fname, multiple, yes_to_all)
+                if yes_to_all is not None and not yes_to_all:
+                    # Canceled
+                    break
 
 
 class ProjectExplorerWidget(QWidget):
@@ -214,7 +231,32 @@ def setup_project(self, directory):
 
         # Setup the directory shown by the tree
         self.set_project_dir(directory)
-
+     
+        # Signal to delete the project
+        self.treewidget.sig_delete_project.connect(self.delete_project)
+
+    def delete_project(self):
+        """Delete current project without deleting the files in the directory."""
+        if self.current_active_project:
+            path = self.current_active_project.root_path
+            buttons = QMessageBox.Yes|QMessageBox.No
+            answer = QMessageBox.warning(self, _("Delete"),
+                                 _("Do you really want "
+                                   "to delete <b>{filename}</b>?<br><br>"
+                                   "<b>Note:</b> This action will only delete "
+                                   "the project. Its files are going to be "
+                                   "preserved on disk."
+                                   ).format(filename=osp.basename(path)),
+                                   buttons)
+            if answer == QMessageBox.Yes:
+                try:
+                    self.close_project()
+                    shutil.rmtree(osp.join(path,'.spyproject'))
+                except EnvironmentError as error:
+                    QMessageBox.critical(self, _("Project Explorer"),
+                                    _("<b>Unable to delete <i>{varpath}</i></b>"
+                                      "<br><br>The error message was:<br>{error}" )
+                                    .format(varpath=path,error=to_text_string(error)))
 
 #==============================================================================
 # Tests


Test patch: