Repository: spyder-ide/spyder

Issue:
Adding applications to file associations is broken on Windows due to unescaped backslashes
## Description

### What steps will reproduce the problem?

<!--- You can use Markdown here --->

1. From a clean config, open the File Associations tab of the Files prefs pane
2. Add any extension (e.g. .txt) to enable the "add" option for external programs
3. Click the add button, and the resulting error appears

I haven't investigated the relevant code thoroughly, but it appears to be due to unescaped backslashes in Windows file paths combined with use of eval. I'd suggest simply using standard forward slashes instead (using replace to convert them before parsing if necessary) since they are equally accepted on Windows as backslashes.

Also, even after I dismiss the error, my cursor remains stuck in the "hourglass" mode that was triggered with the failed launch of the file open dialog.

### Traceback
```python-traceback
  File "C:\Miniconda3\envs\spyder-4\lib\site-packages\spyder\plugins\explorer\widgets\fileassociations.py", line 496, in add_application
    self._dlg_applications = ApplicationsDialog(self)
  File "C:\Miniconda3\envs\spyder-4\lib\site-packages\spyder\plugins\explorer\widgets\fileassociations.py", line 145, in __init__
    self.setup()
  File "C:\Miniconda3\envs\spyder-4\lib\site-packages\spyder\plugins\explorer\widgets\fileassociations.py", line 152, in setup
    apps = get_installed_applications()
  File "C:\Miniconda3\envs\spyder-4\lib\site-packages\spyder\utils\programs.py", line 493, in get_installed_applications
    apps = _get_win_applications()
  File "C:\Miniconda3\envs\spyder-4\lib\site-packages\spyder\utils\programs.py", line 362, in _get_win_applications
    expanded_fpath = literal_eval(expanded_fpath)
  File "C:\Miniconda3\envs\spyder-4\lib\ast.py", line 46, in literal_eval
    node_or_string = parse(node_or_string, mode='eval')
  File "C:\Miniconda3\envs\spyder-4\lib\ast.py", line 35, in parse
    return compile(source, filename, mode, PyCF_ONLY_AST)
  File "<unknown>", line 1
    c:\program files (x86)\malwarebytes' anti-malware\mbam.exe
     ^
SyntaxError: invalid syntax
```

## Versions

* Spyder version: 4.0.0rc1 
* Python version: 3.7.5
* Qt version: 5.9.6
* PyQt5 version: 5.9.2
* Operating System: Windows 8.1

### Dependencies

```
cloudpickle >=0.5.0          :  0.7.0 (OK)
pygments >=2.0               :  2.3.1 (OK)
qtconsole >=4.5.5            :  4.5.5 (OK)
nbconvert >=4.0              :  5.4.0 (OK)
sphinx >=0.6.6               :  1.8.4 (OK)
pylint >=0.25                :  2.2.2 (OK)
psutil >=0.3                 :  5.6.3 (OK)
qtawesome >=0.5.7            :  0.6.0 (OK)
qtpy >=1.5.0                 :  1.9.0 (OK)
pickleshare >=0.4            :  0.7.5 (OK)
zmq >=17                     :  17.1.2 (OK)
chardet >=2.0.0              :  3.0.4 (OK)
numpydoc >=0.6.0             :  0.8.0 (OK)
spyder_kernels >=1.7.0;<2.0.0:  1.7.0 (OK)
qdarkstyle >=2.7             :  2.7 (OK)
atomicwrites >=1.2.0         :  1.3.0 (OK)
diff_match_patch >=20181111  :  20181111 (OK)
watchdog                     :  None (OK)
keyring                      :  None (OK)
pexpect >=4.4.0              :  4.7.0 (OK)
pympler                      :  None (OK)
sympy >=0.7.3                :  1.3 (OK)
cython >=0.21                :  0.29.4 (OK)
IPython >=4.0                :  7.2.0 (OK)
matplotlib >=2.0.0           :  3.0.2 (OK)
pandas >=0.13.1              :  0.24.2 (OK)
numpy >=1.7                  :  1.15.4 (OK)
scipy >=0.17.0               :  1.2.0 (OK)
pyls >=0.29.3;<0.30.0        :  0.29.3 (OK)
rtree >=0.8.3                :  0.8.3 (OK)
```



Discussion:
Also, not sure what's up with the dependency report but ``conda list`` confirms I have all three of watchdog, keyring and pympler installed in my Spyder 4 env and Spyder even used keyring to remember my Github token to submit this report.
@goanpeca, please take a look at this one.
> Also, not sure what's up with the dependency report but conda list confirms I have all three of watchdog, keyring and pympler installed in my Spyder 4 env and Spyder even used keyring to remember my Github token to submit this report.

Please read carefully: they don't have available versions to report (because the packages themselves don't declare them), but they are marked as `OK`.
@ccordoba12 I see, but it isn't clear simply from the context that the `None` was referring to the version as opposed to the installation status, and `OK` to the installation status as opposed to whether lack of that package was a critical problem (e.g. if it was optional).

By the way, even if the package doesn't declare ``__version__`` or similar, its easy to get the version using Setuptools' ``pkg_resources``, since all those packages still have one. Here's a snippit adapted directly from my own production code:

```
import pip._vendor.pkg_resources as pkg_resources  # Or just `import pkg_resources` if you know you'll have setuptools available, like with conda, linux distros, etc
for package in pkg_resources.working_set:
    if package.project_name.lower() == "PACKAGENAME":
        package_version = package.version
        break
```

EDIT: Oops, accidentally hit close. Sorry!
If you found a fix, please submit a pull request for that, along with tests. If not, we'll try to fix that when more important things are solved.
@ccordoba12 Sure, I'll add it to my list. As you say its not at all critical and it should be a quick fix and test for me to do so I'll try to get to it perhaps after the Spyder 4.0 release. Thanks.

Resolving patch:
diff --git a/spyder/utils/programs.py b/spyder/utils/programs.py
--- a/spyder/utils/programs.py
+++ b/spyder/utils/programs.py
@@ -335,6 +335,15 @@ def _get_win_reg_info(key_path, hive, flag, subkeys):
     return software_list
 
 
+def _clean_win_application_path(path):
+    """Normalize windows path and remove extra quotes."""
+    path = path.replace('\\', '/').lower()
+    # Check for quotes at start and end
+    if path[0] == '"' and path[-1] == '"':
+        path = literal_eval(path)
+    return path
+
+
 def _get_win_applications():
     """Return all system installed windows applications."""
     import winreg
@@ -357,9 +366,8 @@ def _get_win_applications():
     for software in sorted(software_list, key=lambda x: x[sort_key]):
         if software[None]:
             key = software['key'].capitalize().replace('.exe', '')
-            expanded_fpath = os.path.expandvars(software[None]).lower()
-            if '"' in expanded_fpath or "'" in expanded_fpath:
-                expanded_fpath = literal_eval(expanded_fpath)
+            expanded_fpath = os.path.expandvars(software[None])
+            expanded_fpath = _clean_win_application_path(expanded_fpath)
             app_paths[key] = expanded_fpath
 
     # See:
@@ -377,7 +385,7 @@ def _get_win_applications():
         icon = software['DisplayIcon']
         key = software['key']
         if name and icon:
-            icon = icon.replace('"', '').replace("'", '')
+            icon = icon.replace('"', '')
             icon = icon.split(',')[0]
 
             if location == '' and icon:
@@ -395,9 +403,9 @@ def _get_win_applications():
                         valid_file = fn_low.endswith(('.exe', '.com', '.bat'))
                         if valid_file and not fn_low.startswith('unins'):
                             fpath = os.path.join(location, fname)
-                            expanded_fpath = os.path.expandvars(fpath.lower())
-                            if '"' in expanded_fpath or "'" in expanded_fpath:
-                                expanded_fpath = literal_eval(expanded_fpath)
+                            expanded_fpath = os.path.expandvars(fpath)
+                            expanded_fpath = _clean_win_application_path(
+                                expanded_fpath)
                             apps[name + ' (' + fname + ')'] = expanded_fpath
     # Join data
     values = list(zip(*apps.values()))[-1]


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
@@ -14,8 +14,8 @@
 import pytest
 
 # Local imports
-from spyder.utils.programs import (check_version, find_program,
-                                   get_application_icon,
+from spyder.utils.programs import (_clean_win_application_path, check_version,
+                                   find_program, get_application_icon,
                                    get_installed_applications, get_temp_dir,
                                    is_module_installed, is_python_interpreter,
                                    is_python_interpreter_valid_name,
@@ -205,6 +205,15 @@ def test_get_temp_dir_ensure_dir_exists():
     assert another_call == temp_dir
 
 
+def test_clean_win_application_path(qtbot):
+    data = (
+        ('"c:\\some thing\\foo.exe"', 'c:/some thing/foo.exe'),
+        ("c:\\some' thing\\foo.exe", "c:/some' thing/foo.exe"),
+    )
+    for path, expected_output in data:
+        assert _clean_win_application_path(path) == expected_output
+
+
 def test_get_installed_apps_and_icons(qtbot):
     apps = get_installed_applications()
     assert apps