Repository: spyder-ide/spyder

Issue:
Run in external system terminal doesn't work in macOS
## Description

**What steps will reproduce the problem?**

1. open python file
2. "run"->"configuration per file.."
3. set "execute in an external system terminal"
4. "run"
5. nothing happens

**What is the expected output? What do you see instead?**

I expected Terminal app appearance, but terminal doesn't load


**Please provide any additional information below**



## Version and main components

* Spyder Version: 3.2.1 
* Python Version: 3.6.1 and 2.7
* Qt Versions: 5.6.2, PyQt5 5.6 on Darwin

## Dependencies
```
pyflakes >=0.6.0 :  1.5.0 (OK)
pycodestyle >=2.3:  2.3.1 (OK)
pygments >=2.0   :  2.2.0 (OK)
pandas >=0.13.1  :  0.20.1 (OK)
numpy >=1.7      :  1.12.1 (OK)
sphinx >=0.6.6   :  1.5.6 (OK)
rope >=0.9.4     :  0.9.4-1 (OK)
jedi >=0.9.0     :  0.10.2 (OK)
psutil >=0.3     :  5.2.2 (OK)
nbconvert >=4.0  :  5.1.1 (OK)
sympy >=0.7.3    :  1.0 (OK)
cython >=0.21    :  0.25.2 (OK)
qtconsole >=4.2.0:  4.3.0 (OK)
IPython >=4.0    :  5.3.0 (OK)
pylint >=0.25    :  1.6.4 (OK)

```

Best regards,
Andrew


Discussion:
We don't have support for running files in system terminals in macOS, but we'll work to add it in a future release.
Thank you!
@juanis2112 I'm about to push a PR with some fixes and improvements to this feature and its tests on Windows and Linux. After that, might you perhaps be able to implement and test a code snippet (e.g. [something like this](https://stackoverflow.com/questions/4404242/programmatically-launch-terminal-app-with-a-specified-command-and-custom-colors) ) to add this functionality? We've had requests for it on our Gitter and it would help bring our macOS platform closer to parity with the others.

Resolving patch:
diff --git a/spyder/utils/programs.py b/spyder/utils/programs.py
--- a/spyder/utils/programs.py
+++ b/spyder/utils/programs.py
@@ -22,6 +22,8 @@
 import subprocess
 import sys
 import tempfile
+import threading
+import time
 
 # Local imports
 from spyder.config.base import _, get_conf_path, is_stable_version
@@ -685,7 +687,7 @@ def run_python_script_in_terminal(fname, wdir, args, interact,
                                  _("It was not possible to run this file in "
                                    "an external terminal"),
                                  QMessageBox.Ok)
-    elif os.name == 'posix':
+    elif sys.platform.startswith('linux'):
         programs = [{'cmd': 'gnome-terminal',
                      'wdir-option': '--working-directory',
                      'execute-option': '-x'},
@@ -710,7 +712,25 @@ def run_python_script_in_terminal(fname, wdir, args, interact,
                 else:
                     run_program(program['cmd'], arglist)
                 return
-        # TODO: Add a fallback to OSX
+    elif sys.platform == 'darwin':
+        f = tempfile.NamedTemporaryFile('wt', prefix='run_spyder_',
+                                        suffix='.sh', dir=get_temp_dir(),
+                                        delete=False)
+        if wdir:
+            f.write('cd {}\n'.format(wdir))
+        f.write(' '.join(p_args))
+        f.close()
+        os.chmod(f.name, 0o777)
+
+        def run_terminal_thread():
+            proc = run_shell_command('open -a Terminal.app ' + f.name)
+            # Prevent race condition
+            time.sleep(3)
+            proc.wait()
+            os.remove(f.name)
+
+        thread = threading.Thread(target=run_terminal_thread)
+        thread.start()
     else:
         raise NotImplementedError
 


Test patch:
diff --git a/spyder/utils/tests/test_programs.py b/spyder/utils/tests/test_programs.py
--- a/spyder/utils/tests/test_programs.py
+++ b/spyder/utils/tests/test_programs.py
@@ -52,6 +52,20 @@ def scriptpath(tmpdir):
     return scriptpath
 
 
+@pytest.fixture
+def scriptpath_with_blanks(tmpdir):
+    """Save a basic Python script in a file."""
+    name_dir = 'test dir'
+    if not osp.exists(name_dir):
+        os.mkdir(name_dir)
+    os.chdir(name_dir)
+    tmpdir.join(name_dir)
+    script = ("with open('out.txt', 'w') as f:\n"
+              "    f.write('done')\n")
+    scriptpath = tmpdir.join('write-done.py')
+    scriptpath.write(script)
+    return scriptpath
+
 # =============================================================================
 # ---- Tests
 # =============================================================================
@@ -65,19 +79,40 @@ def test_is_valid_w_interpreter():
 
 @flaky(max_runs=3)
 @pytest.mark.skipif(
-    os.environ.get('CI', None) is None or sys.platform == 'darwin',
-    reason='fails in macOS and sometimes locally')
+    os.environ.get('CI', None) is None,
+    reason='fails sometimes locally')
 def test_run_python_script_in_terminal(scriptpath, qtbot):
     """
     Test running a Python script in an external terminal when specifying
     explicitely the working directory.
     """
-    # Run the script.
+    # Run the script
     outfilepath = osp.join(scriptpath.dirname, 'out.txt')
     run_python_script_in_terminal(
         scriptpath.strpath, scriptpath.dirname, '', False, False, '')
-    qtbot.waitUntil(lambda: osp.exists(outfilepath), timeout=1000)
+    qtbot.waitUntil(lambda: osp.exists(outfilepath), timeout=5000)
+    # Assert the result.
+    with open(outfilepath, 'r') as txtfile:
+        res = txtfile.read()
+    assert res == 'done'
+
 
+@flaky(max_runs=3)
+@pytest.mark.skipif(
+    os.environ.get('CI', None) is None,
+    reason='fails sometimes locally')
+def test_run_python_script_in_terminal_blank_wdir(scriptpath_with_blanks,
+                                                  qtbot):
+    """
+    Test running a Python script in an external terminal when specifying
+    explicitely the working directory.
+    """
+    # Run the script
+    outfilepath = osp.join(scriptpath_with_blanks.dirname, 'out.txt')
+    run_python_script_in_terminal(
+        scriptpath_with_blanks.strpath, scriptpath_with_blanks.dirname,
+        '', False, False, '')
+    qtbot.waitUntil(lambda: osp.exists(outfilepath), timeout=5000)
     # Assert the result.
     with open(outfilepath, 'r') as txtfile:
         res = txtfile.read()
@@ -86,18 +121,21 @@ def test_run_python_script_in_terminal(scriptpath, qtbot):
 
 @flaky(max_runs=3)
 @pytest.mark.skipif(
-    os.environ.get('CI', None) is None or sys.platform == 'darwin',
-    reason='fails in macOS and sometimes locally')
+    os.environ.get('CI', None) is None,
+    reason='fails sometimes locally')
 def test_run_python_script_in_terminal_with_wdir_empty(scriptpath, qtbot):
     """
     Test running a Python script in an external terminal without specifying
     the working directory.
     """
     # Run the script.
-    outfilepath = osp.join(os.getcwd(), 'out.txt')
-    run_python_script_in_terminal(scriptpath.strpath, '', '', False, False, '')
-    qtbot.waitUntil(lambda: osp.exists(outfilepath), timeout=1000)
+    if sys.platform == 'darwin':
+        outfilepath = osp.join(osp.expanduser('~'), 'out.txt')
+    else:
+        outfilepath = osp.join(os.getcwd(), 'out.txt')
 
+    run_python_script_in_terminal(scriptpath.strpath, '', '', False, False, '')
+    qtbot.waitUntil(lambda: osp.exists(outfilepath), timeout=5000)
     # Assert the result.
     with open(outfilepath, 'r') as txtfile:
         res = txtfile.read()