Repository: spyder-ide/spyder

Issue:
Windows: Can't execute new python file on network drive
New file created by `project pane: new: python file` has UNC file path without drive letter. command line generates the following message when given a UNC path. 

> CMD.EXE was started with the above path as the current directory.
> UNC paths are not supported.  Defaulting to Windows directory.

With the run settings to "execute in an external terminal", running the file appears to do nothing at all. Opening the directory in an explorer window from spyder similarly puts the UNC path into explorer, so bat files from that window can't be executed (which is how I found the error text)

repro steps:
 - windows
 - on a mapped network share
 - create a new project
 - create a new python file
 - can't run file in external terminal with f5
 - close and re-open file
 - file now refers to drive letter
 - can run file with f5

spyder version info:
* Spyder version: 5.4.0  (pip)
* Python version: 3.11.0 64-bit
* Qt version: 5.15.2
* PyQt5 version: 5.15.7
* Operating System: Windows 10



Discussion:
Hey @athompson673, thanks for reporting. This is interesting:

> * create a new python file
> * can't run file in external terminal with f5
> * close and re-open file
> * file now refers to drive letter

Are you saving your new Python file after creating it from the Projects pane?
@ccordoba12 Afaik, the file is created then saved immediately (with the default coding line and docstring). If I type some code, then save it, the content is correctly saved (I went and found the file and opened it separately in notepad++). The filename at the top of the editor widget however remains as a UNC path "\\\\server\\folder\\folder\\test.py" until I close and re-open the file in another way: "Y:\\folder\\test.py". When the file is a UNC path, it cannot be run with CMD.

create new file from project window:

![image](https://user-images.githubusercontent.com/25141140/200685524-4b43a04d-2163-4152-8737-ac5ed30241ff.png)

Example of file after creation:

![image](https://user-images.githubusercontent.com/25141140/200676394-7a32026d-b2f5-43b9-8a6d-0d0b25dd9903.png)

after closing and re-opening the file:

![image](https://user-images.githubusercontent.com/25141140/200685209-823680f4-da12-4a7d-a027-43c0607a006e.png)

in theory this could be side-stepped by editing spyder/utils/programs.run_python_script_in_terminal to re-write the drive to the drive letter (though I can't immediately find a way to convert from a drive to a drive letter, only the reverse..)
I came up with a dirty hack that does the above (re-write drive letter), and I don't like it, but it does allow the file to be run with a UNC path:

starting inside [run_python_script_in_terminal](https://github.com/spyder-ide/spyder/blob/master/spyder/utils/programs.py#L743):
```python3
    if os.name == 'nt':
        if wdir is not None:
            # wdir can come with / as os.sep, so we need to take care of it.
            wdir = wdir.replace('/', '\\')
            #!!! dirty hack (fix for UNC file path cwd not supported on cmd.exe)
            drives = { osp.splitdrive(osp.realpath(chr(x) + ":"))[0].lower(): chr(x) + ":" for x in range(65,91) if osp.exists(chr(x) + ":") } #reverse map available drives to realpath
            drive, tail = osp.splitdrive(wdir)
            if drive.lower() in drives:
                wdir = osp.join(drives[drive], tail) #apply letter drive instead of realpath to wdir
```

Even more testing unfortunately didn't answer any of my questions...

Writing my own `Popen` command in a naked python prompt with `cwd=r"\\some\unc\path"` at least calls the file, but with the error message, and naturally the wrong cwd (which then may cause errors with the script itself). I cannot understand why with sypder no command window appears at all (this should be mostly the same command), and I cannot seem to find the output of the UNC path warning from cmd.exe at all (doesn't show up in internal console): 

![image](https://user-images.githubusercontent.com/25141140/200707999-abaa4df3-eac5-4d38-b008-5b52b4f7bf8e.png)

> The filename at the top of the editor widget however remains as a UNC path "\\server\folder\folder\test.py" until I close and re-open the file in another way: "Y:\folder\test.py"

What do you mean by "another way"? (Sorry if this sounds dumb but I don't understand very well how UNC paths work).

> I came up with a dirty hack that does the above (re-write drive letter), and I don't like it, but it does allow the file to be run with a UNC path

Ok, that's good but why don't you like it?
I must apologize for being a bit of a moving target... I'm partially learning as I go both what the problem is, and what could be done to fix it.

> "another way":
 - file menu > open\*
 - drag & drop from explorer\*
 - double click file from project pane\*

\*unless the file is in an un-mapped network location to begin with

> why don't I like it:
 - It doesn't consider network shares not mapped to a letter.
 - Filesystem is case aware but not sensitive? but not always (depending on windows registry)? I just forced `.lower()` for the quick hack.
 - UNC path is fine literally everywhere else but cmd.exe

Doing a bit more research, I found this [serverfault post](https://serverfault.com/questions/50085/how-do-you-handle-cmd-does-not-support-unc-paths-as-current-directories). Perhaps the command string sent to Popen could be further modified to use `pushd` and `popd` to dynamically map any un-mapped network locations, but that then brings even more questions:
 - Should we leave the drive mapped so future calls to run the program already have the drive mapped? Drive won't be unmapped until logoff or reboot..
 - If we attempt to unmap at the end of a script, what if the user runs the file multiple times concurrently in separate cmd windows?
 - Do we just blindly map the location again even if it already exists so unmapping won't impact any other running scripts? we run out in at most 25 (C is always taken..)

I kinda don't like any of these very much.... I wonder if it's most appropriate to simply figure out how to capture the error text and display it to the user "CMD doesn't support UNC path" rather than hack our way around the problem or worse (as it is now) silently fail.

Maybe this instead of the dirty hack? I suppose it doesn't fix the issue, but it makes it less silent...

```python3
    if os.name == 'nt':
        if wdir is not None:
            # wdir can come with / as os.sep, so we need to take care of it.
            wdir = wdir.replace('/', '\\')

        # python_exe must be quoted in case it has spaces
        cmd = f'start cmd.exe /K ""{executable}" '
        cmd += ' '.join(p_args) + '"' + ' ^&^& exit'
        try:
            popen = run_shell_command(cmd, cwd=wdir)
        except WindowsError:
            from qtpy.QtWidgets import QMessageBox
            from spyder.config.base import _
            QMessageBox.critical(None, _('Run'),
                                 _("It was not possible to run this file in "
                                   "an external terminal"),
                                 QMessageBox.Ok)
        def wait_for_error(popen):
            out, err = popen.communicate()
            if err is None: return
            print(err) #print to internal console to inform user of error. QMessageBox caused an issue...
        threading.Thread(target=wait_for_error, args=(popen,), daemon=True).start()
```
Ok, I like this idea better but I have one suggestion: what if instead of printing the error message in the internal console, we try to add the it to the `QMessageBox` message? Do you think that's feasible?
@ccordoba12 I haven't figured out how to use a QMessageBox in the above manner without it crashing spyder all together.. Perhaps we just try to detect a UNC path, and preempt even trying to run the file? 

```python3
    if os.name == 'nt':
        if wdir is not None:
            # wdir can come with / as os.sep, so we need to take care of it.
            wdir = wdir.replace('/', '\\')
            
        if osp.splitdrive(wdir)[0].startswith("\\\\"): #UNC paths start with \\
            from qtpy.QtWidgets import QMessageBox
            from spyder.config.base import _
            QMessageBox.critical(None, _('Run'),
                                 _("External terminal does not support a UNC "
                                   "file path as the working directory."),
                                 QMessageBox.Ok)
            return
```
> Perhaps we just try to detect a UNC path, and preempt even trying to run the file?

Nice! I really like this solution. Would you mind to open a pull request for it? If that's ok for you, please don't forget to read our [Contributing guide](https://github.com/spyder-ide/spyder/blob/master/CONTRIBUTING.md) first.

If you don't feel comfortable with that, I can do it for you, giving you the respective credit for the fix in the commit message, of course.

Resolving patch:
diff --git a/spyder/utils/programs.py b/spyder/utils/programs.py
--- a/spyder/utils/programs.py
+++ b/spyder/utils/programs.py
@@ -744,6 +744,15 @@ def run_python_script_in_terminal(fname, wdir, args, interact, debug,
         if wdir is not None:
             # wdir can come with / as os.sep, so we need to take care of it.
             wdir = wdir.replace('/', '\\')
+        
+        if osp.splitdrive(wdir)[0].startswith("\\\\"): #UNC paths start with \\
+            from qtpy.QtWidgets import QMessageBox
+            from spyder.config.base import _
+            QMessageBox.critical(None, _('Run'),
+                                 _("External terminal does not support a UNC "
+                                   "file path as the working directory."),
+                                 QMessageBox.Ok)
+            return
 
         # python_exe must be quoted in case it has spaces
         cmd = f'start cmd.exe /K ""{executable}" '


Test patch: