Repository: spyder-ide/spyder
Pull request: PR : Move files to the OS trash can when deleting them in Files or Projects
<!--- 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


<!--- Explain what you've done and why --->




### Issue(s) Resolved

<!--- List the issue(s) below, in the form "Fixes #1234"; one per line --->

Fixes #21659


### 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:
@jsbautista 
<!--- Thanks for your help making Spyder better for everyone! --->


Problem statement:
Files are not moved to the operating system trash can after deleting them in Files or Projects
## Problem Description
Hi,

I've reported this again in the past but the issue has not been resolved. I am running Spyder under Ubuntu and when I try to delete a file from the file explorer it triggers a permanent delete of the folder with all my work. 


### What steps reproduce the problem?

1. It usually gets triggered when running in debug and then deleting a file from the file explorer within Spyder.
2. It might also happen when not running in debug and then deleting any file.

### Paste Traceback/Error Below (if applicable)
No error is shown. Spyder will show a message for every tab saying that the file doesn't exist and if I want to close the file.

## Versions

* Spyder version: 5.4.3
* Python version: 3.7.15 64-bit
* Qt version:  5.15.2
* PyQt version: 5.15.7 
* Operating System name/version: Linux 6.5.0-14-generic

### Dependencies
# Mandatory:
atomicwrites >=1.2.0                             :  1.4.1 (OK)
chardet >=2.0.0                                  :  5.0.0 (OK)
cloudpickle >=0.5.0                              :  2.1.0 (OK)
cookiecutter >=1.6.0                             :  2.1.1 (OK)
diff_match_patch >=20181111                      :  20200713 (OK)
intervaltree >=3.0.2                             :  3.1.0 (OK)
IPython >=7.31.1,<9.0.0,!=8.8.0,!=8.9.0,!=8.10.0 :  7.34.0 (OK)
jedi >=0.17.2,<0.19.0                            :  0.18.1 (OK)
jellyfish >=0.7                                  :  0.9.0 (OK)
jsonschema >=3.2.0                               :  4.14.0 (OK)
keyring >=17.0.0                                 :  23.8.2 (OK)
nbconvert >=4.0                                  :  7.0.0 (OK)
numpydoc >=0.6.0                                 :  1.4.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.9.1 (OK)
pygments >=2.0                                   :  2.13.0 (OK)
pylint >=2.5.0,<3.0                              :  2.14.5 (OK)
pylint_venv >=2.1.1                              :  3.0.1 (OK)
pyls_spyder >=0.4.0                              :  0.4.0 (OK)
pylsp >=1.7.2,<1.8.0                             :  1.7.3 (OK)
pylsp_black >=1.2.0                              :  1.2.1 (OK)
qdarkstyle >=3.0.2,<3.2.0                        :  3.0.3 (OK)
qstylizer >=0.2.2                                :  0.2.2 (OK)
qtawesome >=1.2.1                                :  1.2.3 (OK)
qtconsole >=5.4.2,<5.5.0                         :  5.4.3 (OK)
qtpy >=2.1.0                                     :  2.2.0 (OK)
rtree >=0.9.7                                    :  1.0.0 (OK)
setuptools >=49.6.0                              :  65.3.0 (OK)
sphinx >=0.6.6                                   :  4.3.2 (OK)
spyder_kernels >=2.4.3,<2.5.0                    :  2.4.3 (OK)
textdistance >=4.2.0                             :  4.4.0 (OK)
three_merge >=0.1.1                              :  0.1.1 (OK)
watchdog >=0.10.3                                :  2.1.9 (OK)
xdg >=0.26                                       :  0.28 (OK)
zmq >=22.1.0                                     :  23.2.1 (OK)

# Optional:
cython >=0.21                                    :  0.29.32 (OK)
matplotlib >=3.0.0                               :  3.5.3 (OK)
numpy >=1.7                                      :  1.21.6 (OK)
pandas >=1.1.1                                   :  1.3.5 (OK)
scipy >=0.17.0                                   :  1.7.3 (OK)
sympy >=0.7.3                                    :  None (NOK)

Hints from issue discussion:
Hi @cyiannaki sorry for the late response and not addressing any previous issue! This seems indeed like quite a serious bug! Could you link to this issue the previous ones please?

I did a quick check on Windows but I was unable to reproduce this :thinking: Pinging @ccordoba12 (which maybe could be able to check on Linux). Also, do you have a Spyder project open when this happens?

Any other info to better understand this and reproduce in our side is greatly appreciated!
@jsbautista, Qt provides an API to do this now: https://doc.qt.io/qt-5/qfile.html#moveToTrash

Please use it to solve this issue.

Commit message: Merge branch 'master' into deleteFiles

Patch to review:
diff --git a/spyder/plugins/explorer/widgets/explorer.py b/spyder/plugins/explorer/widgets/explorer.py
index 34fb0578915..a0240489234 100644
--- a/spyder/plugins/explorer/widgets/explorer.py
+++ b/spyder/plugins/explorer/widgets/explorer.py
@@ -23,6 +23,7 @@
 from qtpy.compat import getexistingdirectory, getsavefilename
 from qtpy.QtCore import (
     QDir,
+    QFile,
     QMimeData,
     QSortFilterProxyModel,
     Qt,
@@ -1166,14 +1167,7 @@ def remove_tree(self, dirname):
         Reimplemented in project explorer widget
         """
         while osp.exists(dirname):
-            try:
-                shutil.rmtree(dirname, onerror=misc.onerror)
-            except Exception as e:
-                # This handles a Windows problem with shutil.rmtree.
-                # See spyder-ide/spyder#8567.
-                if type(e).__name__ == "OSError":
-                    error_path = str(e.filename)
-                    shutil.rmtree(error_path, ignore_errors=True)
+            QFile.moveToTrash(dirname)
 
     def delete_file(self, fname, multiple, yes_to_all):
         """Delete file"""
@@ -1185,7 +1179,8 @@ def delete_file(self, fname, multiple, yes_to_all):
         if yes_to_all is None:
             answer = QMessageBox.warning(
                 self, _("Delete"),
-                _("Do you really want to delete <b>%s</b>?"
+                _("Do you really want to delete <b>%s</b>?\n"
+                  "The file will be moved to the trash."
                   ) % osp.basename(fname), buttons)
             if answer == QMessageBox.No:
                 return yes_to_all
diff --git a/spyder/utils/misc.py b/spyder/utils/misc.py
index 50d5b0bbbd8..e92ad296408 100644
--- a/spyder/utils/misc.py
+++ b/spyder/utils/misc.py
@@ -15,6 +15,7 @@
 import stat
 import socket
 
+from qtpy.QtCore import QFile
 from spyder.config.base import get_home_dir
 
 
@@ -26,7 +27,7 @@ def __remove_pyc_pyo(fname):
     if osp.splitext(fname)[1] == '.py':
         for ending in ('c', 'o'):
             if osp.exists(fname + ending):
-                os.remove(fname + ending)
+                QFile.moveToTrash(fname + ending)
 
 
 def rename_file(source, dest):
@@ -43,7 +44,7 @@ def remove_file(fname):
     Remove file *fname*
     If file is a Python script, also rename .pyc and .pyo files if any
     """
-    os.remove(fname)
+    QFile.moveToTrash(fname)
     __remove_pyc_pyo(fname)
 
 


Code review comments:
diff_hunk: @@ -26,7 +27,7 @@ def __remove_pyc_pyo(fname):
     if osp.splitext(fname)[1] == '.py':
         for ending in ('c', 'o'):
             if osp.exists(fname + ending):
-                os.remove(fname + ending)
+                QFile.moveToTrash(fname + ending)
path: spyder/utils/misc.py
text: @user1:
```suggestion
                os.remove(fname + ending)
```

Let's revert this change because I don't think it's important to move pyc and pyo files to the trash can since they'll be recreated by Python on the next import. Plus, they're placed in the `__pycache__` directory in Python 3.

diff_hunk: @@ -1185,7 +1179,8 @@ def delete_file(self, fname, multiple, yes_to_all):
         if yes_to_all is None:
             answer = QMessageBox.warning(
                 self, _("Delete"),
-                _("Do you really want to delete <b>%s</b>?"
+                _("Do you really want to delete <b>%s</b>?\n"
+                  "The file will be moved to the trash."
path: spyder/plugins/explorer/widgets/explorer.py
text: @user1:
```suggestion
                  "The file will be moved to the trash can."
```

Small improvement to this new text.