Repository: spyder-ide/spyder

Issue:
Cannot restore files between different drives in Windows
<!--- **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)
* [ ] 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
* [x] Completed the **Problem Description**, **Steps to Reproduce** and **Version** sections below


## Problem Description
I keep getting this error message when trying to restore files. Spyder is open in drive D and backup file is in drive C.

![image](https://user-images.githubusercontent.com/22060413/51710878-6b23dc80-2022-11e9-9964-bfe6c27f5d20.png)

### What steps reproduce the problem?

1. Closed spyder by Crtl+C while in bootstap mode from CMD, or even close Spyder without saving the file after some changes
2. I always then keep getting a dialog asking to restore the file
3. When I try to restore the file and error is displayed. Clicking OK does nothing!

The error states that the system cannot move the file between different drives. Therefore, I just discard the auto-saved file, to let Spyder launches.

### What is the expected output? What do you see instead?



### Paste Traceback/Error Below (if applicable)
<!--- Copy from error dialog or View > Panes > Internal Console --->

```python-traceback

PASTE TRACEBACK HERE

```

## 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: Spyder beta4.0 under development (forked today). 
* Python version: 3.6
* Qt version: 
* PyQt version: 
* Operating System name/version: windows 10

### Dependencies
<!--- Please go to the menu entry Help > Dependencies,
press the Copy to clipboard button and paste below --->

```
PASTE DEPENDENCIES HERE
```

pygments >=2.0    :  2.3.1 (OK)
qdarkstyle >=2.6.4:  2.6.5 (OK)
sphinx >=0.6.6    :  1.8.3 (OK)
pyls >=0.19.0     :  0.19.0 (OK)
nbconvert >=4.0   :  5.4.0 (OK)
pandas >=0.13.1   :  0.23.4 (OK)
numpy >=1.7       :  1.15.4 (OK)
sympy >=0.7.3     :  1.3 (OK)
cython >=0.21     :  0.29.3 (OK)
qtconsole >=4.2.0 :  4.4.3 (OK)
IPython >=4.0     :  7.2.0 (OK)
matplotlib >=2.0.0:  3.0.2 (OK)
pylint >=0.25     :  2.2.2 (OK)

This might give an idea to the solution.
https://bugs.python.org/issue29805



Discussion:
@jitseniesen, could you take a look at this one? Thanks!
@Khalilsqu Thanks for the report. I think the solution is to change [line 224](https://github.com/spyder-ide/spyder/blob/master/spyder/plugins/editor/widgets/recover.py#L224)  of `recover.py` from
```
except AttributeError:
```
to
```
except (AttributeError, OSError):
```
However, I don't have access to a Windows machine at the moment to test this. Can you please check whether this helps? Thanks.

Resolving patch:
diff --git a/spyder/plugins/editor/widgets/recover.py b/spyder/plugins/editor/widgets/recover.py
--- a/spyder/plugins/editor/widgets/recover.py
+++ b/spyder/plugins/editor/widgets/recover.py
@@ -9,6 +9,7 @@
 # Standard library imports
 from os import path as osp
 import os
+import shutil
 import time
 
 # Third party imports
@@ -20,9 +21,6 @@
 
 # Local imports
 from spyder.config.base import _, running_under_pytest
-from spyder.py3compat import PY2
-if PY2:
-    import shutil
 
 
 def gather_file_data(name):
@@ -221,7 +219,9 @@ def restore(self, idx):
         try:
             try:
                 os.replace(autosave['name'], orig_name)
-            except AttributeError:  # Python 2
+            except (AttributeError, OSError):
+                # os.replace() does not exist on Python 2 and fails if the
+                # files are on different file systems (issue #8631)
                 shutil.copy2(autosave['name'], orig_name)
                 os.remove(autosave['name'])
             self.deactivate(idx)


Test patch:
diff --git a/spyder/plugins/editor/widgets/tests/test_recover.py b/spyder/plugins/editor/widgets/tests/test_recover.py
--- a/spyder/plugins/editor/widgets/tests/test_recover.py
+++ b/spyder/plugins/editor/widgets/tests/test_recover.py
@@ -15,7 +15,7 @@
 from qtpy.QtWidgets import QDialogButtonBox, QPushButton, QTableWidget
 
 # Local imports
-from spyder.py3compat import PY3
+from spyder.py3compat import PY2
 from spyder.plugins.editor.widgets.recover import (make_temporary_files,
                                                    RecoveryDialog)
 
@@ -171,6 +171,29 @@ def test_recoverydialog_restore_when_original_not_recorded(
         assert not table.cellWidget(2, col).isEnabled()
 
 
+def test_recoverydialog_restore_fallback(qtbot, recovery_env, mocker):
+    """
+    Test fallback for when os.replace() fails when recovering a file.
+
+    Test that after pressing the 'Restore' button, if os.replace() fails,
+    the fallback to copy and delete kicks in and the restore succeeds.
+    Regression test for issue #8631.
+    """
+    orig_dir, autosave_dir, autosave_mapping = recovery_env
+    if not PY2:
+        mocker.patch('spyder.plugins.editor.widgets.recover.os.replace',
+                     side_effect=OSError)
+    dialog = RecoveryDialog(autosave_dir, autosave_mapping)
+    table = dialog.findChild(QTableWidget)
+    button = table.cellWidget(0, 2).findChildren(QPushButton)[0]
+    button.click()
+    with open(osp.join(orig_dir, 'ham.py')) as f:
+        assert f.read() == 'ham = "autosave"\n'
+    assert not osp.isfile(osp.join(autosave_dir, 'ham.py'))
+    for col in range(table.columnCount()):
+        assert not table.cellWidget(0, col).isEnabled()
+
+
 def test_recoverydialog_restore_when_error(qtbot, recovery_env, mocker):
     """
     Test that errors during a restore action are handled gracefully.
@@ -180,12 +203,11 @@ def test_recoverydialog_restore_when_error(qtbot, recovery_env, mocker):
     in the grid is not deactivated.
     """
     orig_dir, autosave_dir, autosave_mapping = recovery_env
-    if PY3:
+    if not PY2:
         mocker.patch('spyder.plugins.editor.widgets.recover.os.replace',
                      side_effect=OSError)
-    else:
-        mocker.patch('spyder.plugins.editor.widgets.recover.shutil.copy2',
-                     side_effect=IOError)
+    mocker.patch('spyder.plugins.editor.widgets.recover.shutil.copy2',
+                 side_effect=IOError)
     mock_QMessageBox = mocker.patch(
                 'spyder.plugins.editor.widgets.recover.QMessageBox')
     dialog = RecoveryDialog(autosave_dir, autosave_mapping)