Repository: spyder-ide/spyder

Issue:
Add option to reset per-file run configurations
### The Problem
Once a per-file run configuration is established for a file, there is no convenient way to revert it to follow the global run preference. Currently the only way is to remove the file's entry in the `[run]` section of the `transient.ini` configuration file.
This was first reported in discussion regarding #16200.

### Proposed Solution
Add "Use global run settings" checkbox to the "Run configuration per file" dialog window.
Note that the configuration for any configured file can be accessed from the same dialog using the file dropdown list.
<img width="816" alt="Screen Shot 2021-08-12 at 8 16 04 AM" src="https://user-images.githubusercontent.com/9618975/129224473-20c2345b-daca-4d71-a180-65e53e2b26a2.png">



Discussion:
@spyder-ide/core-developers, any objections or feedback?

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
@@ -58,8 +58,8 @@
                                                   EncodingStatus, EOLStatus,
                                                   ReadWriteStatus, VCSStatus)
 from spyder.plugins.run.widgets import (ALWAYS_OPEN_FIRST_RUN_OPTION,
-                                        get_run_configuration,
-                                        RunConfigDialog, RunConfigOneDialog)
+                                        get_run_configuration, RunConfigDialog,
+                                        RunConfiguration, RunConfigOneDialog)
 from spyder.plugins.mainmenu.api import ApplicationMenus
 
 
@@ -2929,6 +2929,10 @@ def run_file(self, debug=False):
                 return
             runconf = dialog.get_configuration()
 
+        if runconf.default:
+            # use global run preferences settings
+            runconf = RunConfiguration()
+
         args = runconf.get_arguments()
         python_args = runconf.get_python_arguments()
         interact = runconf.interact
diff --git a/spyder/plugins/run/widgets.py b/spyder/plugins/run/widgets.py
--- a/spyder/plugins/run/widgets.py
+++ b/spyder/plugins/run/widgets.py
@@ -28,6 +28,8 @@
 # Localization
 _ = get_translation("spyder")
 
+RUN_DEFAULT_CONFIG = _("Run file with default configuration")
+RUN_CUSTOM_CONFIG = _("Run file with custom configuration")
 CURRENT_INTERPRETER = _("Execute in current console")
 DEDICATED_INTERPRETER = _("Execute in a dedicated console")
 SYSTERM_INTERPRETER = _("Execute in an external system terminal")
@@ -58,6 +60,7 @@ class RunConfiguration(object):
     """Run configuration"""
 
     def __init__(self, fname=None):
+        self.default = None
         self.args = None
         self.args_enabled = None
         self.wdir = None
@@ -78,6 +81,7 @@ def __init__(self, fname=None):
         self.set(CONF.get('run', 'defaultconfiguration', default={}))
 
     def set(self, options):
+        self.default = options.get('default', True)
         self.args = options.get('args', '')
         self.args_enabled = options.get('args/enabled', False)
         self.current = options.get('current',
@@ -104,6 +108,7 @@ def set(self, options):
 
     def get(self):
         return {
+                'default': self.default,
                 'args/enabled': self.args_enabled,
                 'args': self.args,
                 'workdir/enabled': self.wdir_enabled,
@@ -173,8 +178,17 @@ def __init__(self, parent=None):
         self.runconf = RunConfiguration()
         firstrun_o = CONF.get('run', ALWAYS_OPEN_FIRST_RUN_OPTION, False)
 
+        # --- Run settings ---
+        self.run_default_config_radio = QRadioButton(RUN_DEFAULT_CONFIG)
+        self.run_custom_config_radio = QRadioButton(RUN_CUSTOM_CONFIG)
+
         # --- Interpreter ---
         interpreter_group = QGroupBox(_("Console"))
+        interpreter_group.setDisabled(True)
+
+        self.run_custom_config_radio.toggled.connect(
+            interpreter_group.setEnabled)
+
         interpreter_layout = QVBoxLayout(interpreter_group)
 
         self.current_radio = QRadioButton(CURRENT_INTERPRETER)
@@ -186,8 +200,33 @@ def __init__(self, parent=None):
         self.systerm_radio = QRadioButton(SYSTERM_INTERPRETER)
         interpreter_layout.addWidget(self.systerm_radio)
 
+        # --- System terminal ---
+        external_group = QWidget()
+        external_group.setDisabled(True)
+        self.systerm_radio.toggled.connect(external_group.setEnabled)
+
+        external_layout = QGridLayout()
+        external_group.setLayout(external_layout)
+        self.interact_cb = QCheckBox(INTERACT)
+        external_layout.addWidget(self.interact_cb, 1, 0, 1, -1)
+
+        self.pclo_cb = QCheckBox(_("Command line options:"))
+        external_layout.addWidget(self.pclo_cb, 3, 0)
+        self.pclo_edit = QLineEdit()
+        self.pclo_cb.toggled.connect(self.pclo_edit.setEnabled)
+        self.pclo_edit.setEnabled(False)
+        self.pclo_edit.setToolTip(_("<b>-u</b> is added to the "
+                                    "other options you set here"))
+        external_layout.addWidget(self.pclo_edit, 3, 1)
+
+        interpreter_layout.addWidget(external_group)
+
         # --- General settings ----
         common_group = QGroupBox(_("General settings"))
+        common_group.setDisabled(True)
+
+        self.run_custom_config_radio.toggled.connect(common_group.setEnabled)
+
         common_layout = QGridLayout(common_group)
 
         self.clear_var_cb = QCheckBox(CLEAR_ALL_VARIABLES)
@@ -208,6 +247,10 @@ def __init__(self, parent=None):
 
         # --- Working directory ---
         wdir_group = QGroupBox(_("Working directory settings"))
+        wdir_group.setDisabled(True)
+
+        self.run_custom_config_radio.toggled.connect(wdir_group.setEnabled)
+
         wdir_layout = QVBoxLayout(wdir_group)
 
         self.file_dir_radio = QRadioButton(FILE_DIR)
@@ -232,26 +275,6 @@ def __init__(self, parent=None):
         fixed_dir_layout.addWidget(browse_btn)
         wdir_layout.addLayout(fixed_dir_layout)
 
-        # --- System terminal ---
-        external_group = QGroupBox(_("External system terminal"))
-        external_group.setDisabled(True)
-
-        self.systerm_radio.toggled.connect(external_group.setEnabled)
-
-        external_layout = QGridLayout()
-        external_group.setLayout(external_layout)
-        self.interact_cb = QCheckBox(INTERACT)
-        external_layout.addWidget(self.interact_cb, 1, 0, 1, -1)
-
-        self.pclo_cb = QCheckBox(_("Command line options:"))
-        external_layout.addWidget(self.pclo_cb, 3, 0)
-        self.pclo_edit = QLineEdit()
-        self.pclo_cb.toggled.connect(self.pclo_edit.setEnabled)
-        self.pclo_edit.setEnabled(False)
-        self.pclo_edit.setToolTip(_("<b>-u</b> is added to the "
-                                    "other options you set here"))
-        external_layout.addWidget(self.pclo_edit, 3, 1)
-
         # Checkbox to preserve the old behavior, i.e. always open the dialog
         # on first run
         self.firstrun_cb = QCheckBox(ALWAYS_OPEN_FIRST_RUN % _("this dialog"))
@@ -259,10 +282,11 @@ def __init__(self, parent=None):
         self.firstrun_cb.setChecked(firstrun_o)
 
         layout = QVBoxLayout(self)
+        layout.addWidget(self.run_default_config_radio)
+        layout.addWidget(self.run_custom_config_radio)
         layout.addWidget(interpreter_group)
         layout.addWidget(common_group)
         layout.addWidget(wdir_group)
-        layout.addWidget(external_group)
         layout.addWidget(self.firstrun_cb)
         layout.addStretch(100)
 
@@ -278,6 +302,10 @@ def select_directory(self):
 
     def set(self, options):
         self.runconf.set(options)
+        if self.runconf.default:
+            self.run_default_config_radio.setChecked(True)
+        else:
+            self.run_custom_config_radio.setChecked(True)
         self.clo_cb.setChecked(self.runconf.args_enabled)
         self.clo_edit.setText(self.runconf.args)
         if self.runconf.current:
@@ -299,6 +327,7 @@ def set(self, options):
         self.wd_edit.setText(self.dir)
 
     def get(self):
+        self.runconf.default = self.run_default_config_radio.isChecked()
         self.runconf.args_enabled = self.clo_cb.isChecked()
         self.runconf.args = str(self.clo_edit.text())
         self.runconf.current = self.current_radio.isChecked()
diff --git a/spyder/utils/programs.py b/spyder/utils/programs.py
--- a/spyder/utils/programs.py
+++ b/spyder/utils/programs.py
@@ -731,11 +731,16 @@ def run_python_script_in_terminal(fname, wdir, args, interact,
     if executable is None:
         executable = get_python_executable()
 
-    # If fname or python_exe contains spaces, it can't be ran on Windows, so we
+    # If fname or wdir has spaces, must be enclosed in quotes (all platforms)
+    if ' ' in fname:
+        fname = '"' + fname + '"'
+    if ' ' in wdir:
+        wdir = '"' + wdir + '"'
+
+    # If python_exe contains spaces, it can't be ran on Windows, so we
     # have to enclose them in quotes. Also wdir can come with / as os.sep, so
     # we need to take care of it.
     if os.name == 'nt':
-        fname = '"' + fname + '"'
         wdir = wdir.replace('/', '\\')
         executable = '"' + executable + '"'
 


Test patch:
diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py
--- a/spyder/app/tests/test_mainwindow.py
+++ b/spyder/app/tests/test_mainwindow.py
@@ -966,6 +966,7 @@ def test_runconfig_workdir(main_window, qtbot, tmpdir):
 
     # --- Use cwd for this file ---
     rc = RunConfiguration().get()
+    rc['default'] = False
     rc['file_dir'] = False
     rc['cw_dir'] = True
     config_entry = (test_file, rc)
@@ -1022,8 +1023,8 @@ def test_dedicated_consoles(main_window, qtbot):
 
     # --- Set run options for this file ---
     rc = RunConfiguration().get()
-    # A dedicated console is used when these two options are False
-    rc['current'] = rc['systerm'] = False
+    # A dedicated console is used when these three options are False
+    rc['default'] = rc['current'] = rc['systerm'] = False
     config_entry = (test_file, rc)
     CONF.set('run', 'configurations', [config_entry])