Repository: spyder-ide/spyder

Issue:
Possible Black Autoformat Bug

## Problem Description
When I hit F5 to run a script, Spyder will weirdly copy the last few lines of my file, and paste them multiple times at the end of the file. So my file goes from:
```
foo()
bar()
baz()
```
to
```
foo()
bar()
baz()
baz()
baz()
```
### What steps reproduce the problem?
I just upgraded to 4.2. I switched autoformatter to black, and turned on autoformat-on-save. When I edit my file, do not manually run autoformat, and do not manually save, but press F5 to run the script, spyder will trigger the autoformat (fine) but at the same time it also changes the content of the end of the file (not fine). If I manually save the file, or manually run autoformatter, I do not get this problem. Also, this problem does not hit me 100% of the time. It seems most likely to happen when black encounters a long list that it splits across multiple lines.

### What is the expected output? What do you see instead?
When I run a script, I do expect it to save the file and therefore trigger black, but adding new lines to the end of the file is not expected.



Discussion:
Hi @pmh722 thanks for the feedback! I was able to reproduce this issue on Windows with Spyder 4.2.1.

To trigger it you just need to leave at least two empty lines at the beginning of the file:
![black](https://user-images.githubusercontent.com/16781833/106180499-a8d11800-619c-11eb-822e-a8e009d4a000.gif)

Pinging @andfoy and @ccordoba12 just in case
Thanks @dalthviz ! 
Let me know if there's anything more I can do on my end to help debug.
Possibly related, I just got this Spyder "Issue Reporter" trace when I saved a file:
```
  File "{REDACT}/site-packages/spyder/plugins/editor/widgets/editor.py", line 2092, in _save_file
    self.modification_changed(index=index)
  File "{REDACT}/site-packages/spyder/plugins/editor/widgets/editor.py", line 2549, in modification_changed
    finfo = self.data[index]
IndexError: list index out of range
Traceback (most recent call last):
  File "{REDACT}/site-packages/spyder/plugins/editor/widgets/editor.py", line 2092, in _save_file
    self.modification_changed(index=index)
  File "{REDACT}/site-packages/spyder/plugins/editor/widgets/editor.py", line 2549, in modification_changed
    finfo = self.data[index]
IndexError: list index out of range
Traceback (most recent call last):
  File "{REDACT}/site-packages/spyder/plugins/editor/widgets/editor.py", line 2092, in _save_file
    self.modification_changed(index=index)
  File "{REDACT}/site-packages/spyder/plugins/editor/widgets/editor.py", line 2549, in modification_changed
    finfo = self.data[index]
IndexError: list index out of range
Traceback (most recent call last):
  File "{REDACT}/site-packages/spyder/plugins/editor/widgets/editor.py", line 2092, in _save_file
    self.modification_changed(index=index)
  File "{REDACT}/site-packages/spyder/plugins/editor/widgets/editor.py", line 2549, in modification_changed
    finfo = self.data[index]
IndexError: list index out of range

```

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
@@ -2567,72 +2567,72 @@ def edit_run_configurations(self):
     def run_file(self, debug=False):
         """Run script inside current interpreter or in a new one"""
         editorstack = self.get_current_editorstack()
-        if editorstack.save(save_new_files=False):
-            editor = self.get_current_editor()
-            fname = osp.abspath(self.get_current_filename())
-
-            # Get fname's dirname before we escape the single and double
-            # quotes. Fixes spyder-ide/spyder#6771.
-            dirname = osp.dirname(fname)
 
-            # Escape single and double quotes in fname and dirname.
-            # Fixes spyder-ide/spyder#2158.
-            fname = fname.replace("'", r"\'").replace('"', r'\"')
-            dirname = dirname.replace("'", r"\'").replace('"', r'\"')
+        editor = self.get_current_editor()
+        fname = osp.abspath(self.get_current_filename())
 
-            runconf = get_run_configuration(fname)
-            if runconf is None:
-                dialog = RunConfigOneDialog(self)
-                dialog.size_change.connect(lambda s: self.set_dialog_size(s))
-                if self.dialog_size is not None:
-                    dialog.resize(self.dialog_size)
-                dialog.setup(fname)
-                if CONF.get('run', 'open_at_least_once',
-                            not running_under_pytest()):
-                    # Open Run Config dialog at least once: the first time
-                    # a script is ever run in Spyder, so that the user may
-                    # see it at least once and be conscious that it exists
-                    show_dlg = True
-                    CONF.set('run', 'open_at_least_once', False)
-                else:
-                    # Open Run Config dialog only
-                    # if ALWAYS_OPEN_FIRST_RUN_OPTION option is enabled
-                    show_dlg = CONF.get('run', ALWAYS_OPEN_FIRST_RUN_OPTION)
-                if show_dlg and not dialog.exec_():
-                    return
-                runconf = dialog.get_configuration()
-
-            args = runconf.get_arguments()
-            python_args = runconf.get_python_arguments()
-            interact = runconf.interact
-            post_mortem = runconf.post_mortem
-            current = runconf.current
-            systerm = runconf.systerm
-            clear_namespace = runconf.clear_namespace
-            console_namespace = runconf.console_namespace
-
-            if runconf.file_dir:
-                wdir = dirname
-            elif runconf.cw_dir:
-                wdir = ''
-            elif osp.isdir(runconf.dir):
-                wdir = runconf.dir
+        # Get fname's dirname before we escape the single and double
+        # quotes. Fixes spyder-ide/spyder#6771.
+        dirname = osp.dirname(fname)
+
+        # Escape single and double quotes in fname and dirname.
+        # Fixes spyder-ide/spyder#2158.
+        fname = fname.replace("'", r"\'").replace('"', r'\"')
+        dirname = dirname.replace("'", r"\'").replace('"', r'\"')
+
+        runconf = get_run_configuration(fname)
+        if runconf is None:
+            dialog = RunConfigOneDialog(self)
+            dialog.size_change.connect(lambda s: self.set_dialog_size(s))
+            if self.dialog_size is not None:
+                dialog.resize(self.dialog_size)
+            dialog.setup(fname)
+            if CONF.get('run', 'open_at_least_once',
+                        not running_under_pytest()):
+                # Open Run Config dialog at least once: the first time
+                # a script is ever run in Spyder, so that the user may
+                # see it at least once and be conscious that it exists
+                show_dlg = True
+                CONF.set('run', 'open_at_least_once', False)
             else:
-                wdir = ''
-
-            python = True # Note: in the future, it may be useful to run
-            # something in a terminal instead of a Python interp.
-            self.__last_ec_exec = (fname, wdir, args, interact, debug,
-                                   python, python_args, current, systerm,
-                                   post_mortem, clear_namespace,
-                                   console_namespace)
-            self.re_run_file(save_new_files=False)
-            if not interact and not debug:
-                # If external console dockwidget is hidden, it will be
-                # raised in top-level and so focus will be given to the
-                # current external shell automatically
-                # (see SpyderPluginWidget.visibility_changed method)
-                editor.setFocus()
+                # Open Run Config dialog only
+                # if ALWAYS_OPEN_FIRST_RUN_OPTION option is enabled
+                show_dlg = CONF.get('run', ALWAYS_OPEN_FIRST_RUN_OPTION)
+            if show_dlg and not dialog.exec_():
+                return
+            runconf = dialog.get_configuration()
+
+        args = runconf.get_arguments()
+        python_args = runconf.get_python_arguments()
+        interact = runconf.interact
+        post_mortem = runconf.post_mortem
+        current = runconf.current
+        systerm = runconf.systerm
+        clear_namespace = runconf.clear_namespace
+        console_namespace = runconf.console_namespace
+
+        if runconf.file_dir:
+            wdir = dirname
+        elif runconf.cw_dir:
+            wdir = ''
+        elif osp.isdir(runconf.dir):
+            wdir = runconf.dir
+        else:
+            wdir = ''
+
+        python = True  # Note: in the future, it may be useful to run
+        # something in a terminal instead of a Python interp.
+        self.__last_ec_exec = (fname, wdir, args, interact, debug,
+                               python, python_args, current, systerm,
+                               post_mortem, clear_namespace,
+                               console_namespace)
+        self.re_run_file(save_new_files=False)
+        if not interact and not debug:
+            # If external console dockwidget is hidden, it will be
+            # raised in top-level and so focus will be given to the
+            # current external shell automatically
+            # (see SpyderPluginWidget.visibility_changed method)
+            editor.setFocus()
 
     def set_dialog_size(self, size):
         self.dialog_size = size
@@ -2650,7 +2650,9 @@ def debug_file(self):
     def re_run_file(self, save_new_files=True):
         """Re-run last script"""
         if self.get_option('save_all_before_run'):
-            self.save_all(save_new_files=save_new_files)
+            all_saved = self.save_all(save_new_files=save_new_files)
+            if all_saved is not None and not all_saved:
+                return
         if self.__last_ec_exec is None:
             return
         (fname, wdir, args, interact, debug,
diff --git a/spyder/plugins/editor/widgets/editor.py b/spyder/plugins/editor/widgets/editor.py
--- a/spyder/plugins/editor/widgets/editor.py
+++ b/spyder/plugins/editor/widgets/editor.py
@@ -2265,9 +2265,11 @@ def save_all(self, save_new_files=True):
 
         Iterate through self.data and call save() on any modified files.
         """
+        all_saved = True
         for index in range(self.get_stack_count()):
             if self.data[index].editor.document().isModified():
-                self.save(index, save_new_files=save_new_files)
+                all_saved &= self.save(index, save_new_files=save_new_files)
+        return all_saved
 
     #------ Update UI
     def start_stop_analysis_timer(self):


Test patch: