Repository: spyder-ide/spyder

Issue:
Renaming a python file in the file explorer causes an error
## Description

### What steps will reproduce the problem?

<!--- You can use Markdown here --->

Had a "mobilenetv2.py" file. Renamed it via rightclick rename procedure. Typed new file name as "MobileNetV2.py". Spyder asks "do  you want to rename and replace the file". Accepted --> Crash --> Send the bug--> Profit! 

### Traceback
```python-traceback
  File "C:\Users\Tulpar\Anaconda3\lib\site-packages\spyder\plugins\editor\plugin.py", line 2108, in renamed
    filename, to_text_string(dest))
  File "C:\Users\Tulpar\Anaconda3\lib\site-packages\spyder\plugins\editor\utils\autosave.py", line 385, in file_renamed
    old_hash = self.file_hashes[old_name]
KeyError: 'C:\\ferfiles\\mobilenetv2.py'
```

## Versions

* Spyder version: 4.0.1 
* Python version: 3.7.3
* Qt version: 5.9.6
* PyQt5 version: 5.9.2
* Operating System: Windows 10

### Dependencies

```
atomicwrites >=1.2.0         :  1.3.0 (OK)
chardet >=2.0.0              :  3.0.4 (OK)
cloudpickle >=0.5.0          :  1.2.2 (OK)
diff_match_patch >=20181111  :  20181111 (OK)
intervaltree                 :  None (OK)
IPython >=4.0                :  7.11.1 (OK)
jedi =0.14.1                 :  0.14.1 (OK)
nbconvert >=4.0              :  5.6.1 (OK)
numpydoc >=0.6.0             :  0.9.2 (OK)
pexpect >=4.4.0              :  4.7.0 (OK)
pickleshare >=0.4            :  0.7.5 (OK)
psutil >=0.3                 :  5.6.7 (OK)
pygments >=2.0               :  2.5.2 (OK)
pylint >=0.25                :  2.4.4 (OK)
pyls >=0.31.2;<0.32.0        :  0.31.2 (OK)
zmq >=17                     :  18.1.0 (OK)
qdarkstyle >=2.7             :  2.7 (OK)
qtawesome >=0.5.7            :  0.6.0 (OK)
qtconsole >=4.6.0            :  4.6.0 (OK)
qtpy >=1.5.0                 :  1.9.0 (OK)
rtree >=0.8.3                :  0.8.3 (OK)
sphinx >=0.6.6               :  2.3.1 (OK)
spyder_kernels >=1.8.1;<2.0.0:  1.8.1 (OK)
watchdog                     :  None (OK)
cython >=0.21                :  0.29.14 (OK)
matplotlib >=2.0.0           :  3.1.1 (OK)
numpy >=1.7                  :  1.17.4 (OK)
pandas >=0.13.1              :  0.25.3 (OK)
scipy >=0.17.0               :  1.3.2 (OK)
sympy >=0.7.3                :  1.5 (OK)
```



Discussion:
I think file names on Windows are case insensitve, so there's no difference between `mobilenetv2.py` and `MobileNetV2.py`, and that's why the error appears.

@jitseniesen, could you take a look at this one?
> I think file names on Windows are case insensitve, so there's no difference between `mobilenetv2.py` and `MobileNetV2.py`, and that's why the error appears.

Well spotted!

> @jitseniesen, could you take a look at this one?

Will do.
I am trying to understand the traceback, which led me to the File Explorer, in particular line 904-908:

https://github.com/spyder-ide/spyder/blob/6b8c3f1dae50be1c49ecbe3424cc22965d9f9a44/spyder/plugins/explorer/widgets/explorer.py#L904-L908

I wonder whether the second line above should have `path` instead of `fname`, so that it reads 
```
if osp.isfile(path):
``` 
After the `rename_file`, the file `fname` is renamed to `path`, so we should call `osp.file` on the new name. 

Can somebody please check whether that makes sense? I am hesitant to touch code that I don't really understand, and there don't seem to be any adverse effects at the moment.
> Can somebody please check whether that makes sense?

@goanpeca, please help @jitseniesen with that.
Will do!

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
@@ -2096,7 +2096,8 @@ def renamed(self, source, dest):
         Propagate file rename to editor stacks and autosave component.
 
         This function is called when a file is renamed in the file explorer
-        widget or the project explorer.
+        widget or the project explorer. The file may not be opened in the
+        editor.
         """
         filename = osp.abspath(to_text_string(source))
         index = self.editorstacks[0].has_filename(filename)
@@ -2104,8 +2105,8 @@ def renamed(self, source, dest):
             for editorstack in self.editorstacks:
                 editorstack.rename_in_data(filename,
                                            new_filename=to_text_string(dest))
-        self.editorstacks[0].autosave.file_renamed(
-            filename, to_text_string(dest))
+            self.editorstacks[0].autosave.file_renamed(
+                filename, to_text_string(dest))
 
     def renamed_tree(self, source, dest):
         """Directory was renamed in file explorer or in project explorer."""
diff --git a/spyder/plugins/explorer/widgets/explorer.py b/spyder/plugins/explorer/widgets/explorer.py
--- a/spyder/plugins/explorer/widgets/explorer.py
+++ b/spyder/plugins/explorer/widgets/explorer.py
@@ -902,7 +902,7 @@ def rename_file(self, fname):
                     return
             try:
                 misc.rename_file(fname, path)
-                if osp.isfile(fname):
+                if osp.isfile(path):
                     self.sig_renamed.emit(fname, path)
                 else:
                     self.sig_renamed_tree.emit(fname, path)


Test patch:
diff --git a/spyder/plugins/editor/tests/test_plugin.py b/spyder/plugins/editor/tests/test_plugin.py
--- a/spyder/plugins/editor/tests/test_plugin.py
+++ b/spyder/plugins/editor/tests/test_plugin.py
@@ -172,11 +172,27 @@ def test_closing_editor_plugin_stops_autosave_timer(editor_plugin):
     assert not editor.autosave.timer.isActive()
 
 
-def test_renamed_propagates_to_autosave(editor_plugin, mocker):
-    editorstack = editor_plugin.get_current_editorstack()
+def test_renamed_propagates_to_autosave(editor_plugin_open_files, mocker):
+    """Test that editor.renamed() propagates info to autosave component if,
+    and only if, renamed file is open in editor.
+
+    Regression test for spyder-ide/spyder#11348"""
+    editor_factory = editor_plugin_open_files
+    editor, expected_filenames, expected_current_filename = (
+        editor_factory(None, None))
+
+    editorstack = editor.get_current_editorstack()
+    mocker.patch.object(editorstack, 'rename_in_data')
     mocker.patch.object(editorstack.autosave, 'file_renamed')
-    editor_plugin.renamed('src', 'dest')
-    editorstack.autosave.file_renamed.assert_called()
+
+    # Test renaming a file that is not opened in the editor
+    editor.renamed('nonexisting', 'newname')
+    assert not editorstack.autosave.file_renamed.called
+
+    # Test renaming a file that is opened in the editor
+    filename = editorstack.get_filenames()[0]
+    editor.renamed(filename, 'newname')
+    assert editorstack.autosave.file_renamed.called
 
 
 def test_go_to_prev_next_cursor_position(editor_plugin, python_files):
diff --git a/spyder/plugins/explorer/widgets/tests/test_explorer.py b/spyder/plugins/explorer/widgets/tests/test_explorer.py
--- a/spyder/plugins/explorer/widgets/tests/test_explorer.py
+++ b/spyder/plugins/explorer/widgets/tests/test_explorer.py
@@ -17,7 +17,7 @@
 import pytest
 from qtpy.QtCore import QEvent, QPoint, Qt, QTimer
 from qtpy.QtGui import QMouseEvent
-from qtpy.QtWidgets import QApplication, QMenu, QMessageBox
+from qtpy.QtWidgets import QApplication, QInputDialog, QMenu, QMessageBox
 
 # Local imports
 from spyder.plugins.explorer.widgets.explorer import (FileExplorerTest,
@@ -163,6 +163,26 @@ def test_delete_file(explorer_with_files, mocker):
     assert not osp.exists(top_folder)
 
 
+def test_rename_file_with_files(explorer_with_files, mocker, qtbot):
+    """Test that rename_file renames the file and sends out right signal."""
+    project, __, file_paths, __, __ = explorer_with_files
+    for old_path in file_paths:
+        if osp.isfile(old_path):
+            old_basename = osp.basename(old_path)
+            new_basename = 'new' + old_basename
+            new_path = osp.join(osp.dirname(old_path), new_basename)
+            mocker.patch.object(QInputDialog, 'getText',
+                                return_value=(new_basename, True))
+            treewidget = project.explorer.treewidget
+            with qtbot.waitSignal(treewidget.sig_renamed) as blocker:
+                treewidget.rename_file(old_path)
+            assert blocker.args == [old_path, new_path]
+            assert not osp.exists(old_path)
+            with open(new_path, 'r') as fh:
+                text = fh.read()
+            assert text == "File Path:\n" + str(old_path).replace(os.sep, '/')
+
+
 def test_single_click_to_open(qtbot, file_explorer):
     """Test single and double click open option for the file explorer."""
     file_explorer.show()