Repository: spyder-ide/spyder

Issue:
I closed an autosave file and Spyder said there is some error
## Description

### What steps will reproduce the problem?

<!--- You can use Markdown here --->

1. I was using Spyder when there was a power cut.
2. When I turned my computer on and opened up Sypder, it said that there was an autosave file that I could delete or open
3. I selected the option to open it
4. I opened another file in Spyder and then I clicked on the cross icon to close the autosaved file.
5. The file closed but an issue reporter dialog box (this box) opened up

### Traceback
```python-traceback
  File "C:\Users\xxx\Anaconda3\lib\site-packages\spyder\plugins\editor\widgets\editor.py", line 1705, in close_file
    is_ok = (force or self.save_if_changed(cancelable=True, index=index)
  File "C:\Users\xxx\Anaconda3\lib\site-packages\spyder\plugins\editor\widgets\editor.py", line 1866, in save_if_changed
    if not self.save(index):
  File "C:\Users\xxx\Anaconda3\lib\site-packages\spyder\plugins\editor\widgets\editor.py", line 1970, in save
    self.autosave.remove_autosave_file(finfo.filename)
  File "C:\Users\xxx\Anaconda3\lib\site-packages\spyder\plugins\editor\utils\autosave.py", line 313, in remove_autosave_file
    del self.file_hashes[autosave_filename]
KeyError: 'C:\\Users\\xxx\\.spyder-py3\\autosave\\temp.py'
```

## Versions

* Spyder version: 4.1.3 
* Python version: 3.7.6
* Qt version: 5.9.6
* PyQt5 version: 5.9.2
* Operating System: Windows 8.1

### Dependencies

```

# Mandatory:
atomicwrites >=1.2.0           :  1.4.0 (OK)
chardet >=2.0.0                :  3.0.4 (OK)
cloudpickle >=0.5.0            :  1.4.1 (OK)
diff_match_patch >=20181111    :  20181111 (OK)
intervaltree                   :  None (OK)
IPython >=4.0                  :  7.13.0 (OK)
jedi =0.15.2                   :  0.15.2 (OK)
nbconvert >=4.0                :  5.6.1 (OK)
numpydoc >=0.6.0               :  0.9.2 (OK)
paramiko >=2.4.0               :  2.7.1 (OK)
parso =0.5.2                   :  0.5.2 (OK)
pexpect >=4.4.0                :  4.8.0 (OK)
pickleshare >=0.4              :  0.7.5 (OK)
psutil >=5.3                   :  5.7.0 (OK)
pygments >=2.0                 :  2.6.1 (OK)
pylint >=0.25                  :  2.5.0 (OK)
pyls >=0.31.9;<0.32.0          :  0.31.10 (OK)
qdarkstyle >=2.8               :  2.8.1 (OK)
qtawesome >=0.5.7              :  0.7.0 (OK)
qtconsole >=4.6.0              :  4.7.3 (OK)
qtpy >=1.5.0                   :  1.9.0 (OK)
rtree >=0.8.3                  :  0.9.4 (OK)
sphinx >=0.6.6                 :  3.0.3 (OK)
spyder_kernels >=1.9.1;<1.10.0 :  1.9.1 (OK)
watchdog                       :  None (OK)
zmq >=17                       :  18.1.1 (OK)

# Optional:
cython >=0.21                  :  0.29.17 (OK)
matplotlib >=2.0.0             :  3.1.3 (OK)
numpy >=1.7                    :  1.18.1 (OK)
pandas >=0.13.1                :  1.0.3 (OK)
scipy >=0.17.0                 :  1.4.1 (OK)
sympy >=0.7.3                  :  1.5.1 (OK)
```



Discussion:
Thank you for your report. I don't understand precisely what happened in your case, but I can see that something is going wrong when you open an autosaved file and then change the file: it looks like the system creates another autosave file with the same name. 

Sorry about this!

Resolving patch:
diff --git a/spyder/plugins/editor/utils/autosave.py b/spyder/plugins/editor/utils/autosave.py
--- a/spyder/plugins/editor/utils/autosave.py
+++ b/spyder/plugins/editor/utils/autosave.py
@@ -253,16 +253,21 @@ def create_unique_autosave_filename(self, filename, autosave_dir):
         """
         Create unique autosave file name for specified file name.
 
+        The created autosave file name does not yet exist either in
+        `self.name_mapping` or on disk.
+
         Args:
             filename (str): original file name
             autosave_dir (str): directory in which autosave files are stored
         """
         basename = osp.basename(filename)
         autosave_filename = osp.join(autosave_dir, basename)
-        if autosave_filename in self.name_mapping.values():
+        if (autosave_filename in self.name_mapping.values()
+                or osp.exists(autosave_filename)):
             counter = 0
             root, ext = osp.splitext(basename)
-            while autosave_filename in self.name_mapping.values():
+            while (autosave_filename in self.name_mapping.values()
+                   or osp.exists(autosave_filename)):
                 counter += 1
                 autosave_basename = '{}-{}{}'.format(root, counter, ext)
                 autosave_filename = osp.join(autosave_dir, autosave_basename)


Test patch:
diff --git a/spyder/plugins/editor/utils/tests/test_autosave.py b/spyder/plugins/editor/utils/tests/test_autosave.py
--- a/spyder/plugins/editor/utils/tests/test_autosave.py
+++ b/spyder/plugins/editor/utils/tests/test_autosave.py
@@ -7,6 +7,7 @@
 
 # Standard library imports
 import ast
+import os.path as osp
 
 # Third party imports
 import pytest
@@ -146,6 +147,34 @@ def test_try_recover(mocker, tmpdir, error_on_remove):
         assert not pidfile.check()
 
 
+@pytest.mark.parametrize('in_mapping,on_disk',
+                         [(False, False), (True, False), (False, True)])
+def test_create_unique_autosave_filename(mocker, in_mapping, on_disk):
+    """Test that AutosaveForStack.create_unique_autosave_filename() returns
+    a file name in the autosave directory with the same base name as the
+    original file name, unless that already exists in the autosave mapping
+    or on disk."""
+    def new_exists(path):
+        if path == osp.join('autosave', 'ham.py'):
+            return on_disk
+        else:
+            return False
+
+    mocker.patch('os.path.exists', side_effect=new_exists)
+    addon = AutosaveForStack(mocker.Mock())
+    if in_mapping:
+        addon.name_mapping = {osp.join('somedir', 'ham.py'):
+                              osp.join('autosave', 'ham.py')}
+
+    autosave_filename = addon.create_unique_autosave_filename(
+        osp.join('orig', 'ham.py'), 'autosave')
+
+    if in_mapping or on_disk:
+        assert autosave_filename == osp.join('autosave', 'ham-1.py')
+    else:
+        assert autosave_filename == osp.join('autosave', 'ham.py')
+
+
 @pytest.mark.parametrize('have_hash', [True, False])
 def test_autosave(mocker, have_hash):
     """Test that AutosaveForStack.maybe_autosave writes the contents to the
@@ -237,6 +266,21 @@ def test_autosave_remove_autosave_file(mocker, exception):
     assert mock_dialog.called == exception
 
 
+def test_get_autosave_filename(mocker, tmpdir):
+    """Test that AutosaveForStack.get_autosave_filename returns a consistent
+    and unique name for the autosave file is returned."""
+    addon = AutosaveForStack(mocker.Mock())
+    mocker.patch('spyder.plugins.editor.utils.autosave.get_conf_path',
+                 return_value=str(tmpdir))
+
+    expected = str(tmpdir.join('foo.py'))
+    assert addon.get_autosave_filename('foo.py') == expected
+
+    expected2 = str(tmpdir.join('foo-1.py'))
+    assert addon.get_autosave_filename('foo.py') == expected
+    assert addon.get_autosave_filename('ham/foo.py') == expected2
+
+
 @pytest.mark.parametrize('have_hash', [True, False])
 def test_autosave_file_renamed(mocker, tmpdir, have_hash):
     """Test that AutosaveForStack.file_renamed removes the old autosave file,
diff --git a/spyder/plugins/editor/widgets/tests/test_editor.py b/spyder/plugins/editor/widgets/tests/test_editor.py
--- a/spyder/plugins/editor/widgets/tests/test_editor.py
+++ b/spyder/plugins/editor/widgets/tests/test_editor.py
@@ -656,22 +656,6 @@ def test_tab_copies_find_to_replace(editor_find_replace_bot, qtbot):
     assert finder.replace_text.currentText() == 'This is some test text!'
 
 
-def test_get_autosave_filename(editor_bot):
-    """
-    Test filename returned by `get_autosave_filename`.
-
-    Test a consistent and unique name for the autosave file is returned.
-    """
-    editor_stack, editor = editor_bot
-    autosave = editor_stack.autosave
-    expected = os.path.join(get_conf_path('autosave'), 'foo.py')
-    assert autosave.get_autosave_filename('foo.py') == expected
-    editor_stack.new('ham/foo.py', 'utf-8', '')
-    expected2 = os.path.join(get_conf_path('autosave'), 'foo-1.py')
-    assert autosave.get_autosave_filename('foo.py') == expected
-    assert autosave.get_autosave_filename('ham/foo.py') == expected2
-
-
 def test_autosave_all(editor_bot, mocker):
     """
     Test that `autosave_all()` calls maybe_autosave() on all open buffers.
@@ -696,8 +680,9 @@ def test_maybe_autosave(editor_bot):
     editor_stack, editor = editor_bot
     editor.set_text('spam\n')
     editor_stack.autosave.maybe_autosave(0)
-    contents = open(os.path.join(get_conf_path('autosave'), 'foo.py')).read()
-    assert contents == 'spam\n'
+    autosave_filename = os.path.join(get_conf_path('autosave'), 'foo.py')
+    assert open(autosave_filename).read() == 'spam\n'
+    os.remove(autosave_filename)
 
 
 def test_maybe_autosave_saves_only_if_changed(editor_bot, mocker):
@@ -803,7 +788,6 @@ def test_maybe_autosave_does_not_save_after_reload(base_editor_bot, mocker):
     editor_stack.autosave.maybe_autosave(0)
     editor_stack._write_to_file.assert_not_called()
 
-
 def test_autosave_updates_name_mapping(editor_bot, mocker, qtbot):
     """Test that maybe_autosave() updates name_mapping."""
     editor_stack, editor = editor_bot