Repository: spyder-ide/spyder

Issue:
IPython console run code lines not being saved in preferences
## Description of your problem

I am running Spyder 2.3.9 on Windows 7, using the Anaconda Python 3.5.2 distribution. Anaconda is up to date on my system. I can't get Spyder to remember "run code" preference for ipython console. In particular, using the Spyder gui, I click on Tools -> Preferences -> IPython console -> Startup tab. I enter something in the Run code lines: dialog, like print('hello world'). Click Apply, OK. If I go back to preferences, this box is empty. If I start an ipython console, the print statement isn't executed.

The specific problem that got me on this is setting how floating-point numbers are formatted. By default, ipython in Spyder doesn't use scientific notation until many decimal places. I wanted to add the ipython magic command '%precision %.4g'. Can't add it to run code lines dialog. Added to my ipython_config.py, works when I start a free-standing ipython from a prompt. Not in Spyder.

**What steps will reproduce the problem?**
1.  Tools -> Preferences -> IPython console -> Startup -> 
2. Enter text in Run code Lines, e.g. print('foo')
3. Click OK
4. Go back to that dialog box, Lines field is empty
## Versions and main components
- Spyder Version: 2.3.9
- Python Version: 2.5.2
- Operating system: Windows 7.
## Dependencies

Please go to the menu entry `Help > Optional Dependencies` (or
`Help > Dependencies`), press the button `Copy to clipboard`
and paste the contents below:

IPython >=1.0      :  5.0.0 (OK)
jedi >=0.8.1;<0.9.0:  0.9.0 (NOK)
matplotlib >=1.0   :  1.5.1 (OK)
pandas >=0.13.1    :  0.18.1 (OK)
pep8 >=0.6         :  1.7.0 (OK)
pyflakes >=0.6.0   :  1.2.3 (OK)
pygments >=1.6     :  2.1.3 (OK)
pylint >=0.25      :  None (NOK)
qtconsole >=4.0    :  4.2.1 (OK)
rope >=0.9.2       :  0.9.4-1 (OK)
sphinx >=0.6.6     :  1.3.1 (OK)
sympy >=0.7.3      :  1.0 (OK)
zmq >=2.1.11       :  15.4.0 (OK)



Discussion:
Thank you for your report. This issue is still present in the latest version. The problem is that when loading the config file, the string is `eval()`ed. I'm looking into it.

@jitseniesen, the problem is settings location changed in the transition from IPython to Jupyter.

I think I already fixed this error in PR #2878 ;-)

It looks like there several problems. I don't think your PR fixes the issue described here; I tried it the steps under "What steps will reproduce the problem?" with your branch and the issue persists. It has to do with the config system and not with the IPython plugin.

Ok, I see. Then please go ahead and fix it :-)


Resolving patch:
diff --git a/spyder/config/user.py b/spyder/config/user.py
--- a/spyder/config/user.py
+++ b/spyder/config/user.py
@@ -13,6 +13,7 @@
 from __future__ import print_function
 
 # Std imports
+import ast
 import os
 import re
 import os.path as osp
@@ -20,7 +21,6 @@
 import time
 
 # Local imports
-from spyder import __version__
 from spyder.config.base import (get_conf_path, get_home_dir,
                                 get_module_source_path, TEST)
 from spyder.utils.programs import check_version
@@ -378,9 +378,10 @@ def get(self, section, option, default=NoDefault):
                 return default
             
         value = cp.ConfigParser.get(self, section, option, raw=self.raw)
+        # Use type of default_value to parse value correctly
         default_value = self.get_default(section, option)
         if isinstance(default_value, bool):
-            value = eval(value)
+            value = ast.literal_eval(value)
         elif isinstance(default_value, float):
             value = float(value)
         elif isinstance(default_value, int):
@@ -393,8 +394,8 @@ def get(self, section, option, default=NoDefault):
                     pass
             try:
                 # lists, tuples, ...
-                value = eval(value)
-            except:
+                value = ast.literal_eval(value)
+            except (SyntaxError, ValueError):
                 pass
         return value
 


Test patch:
diff --git a/spyder/config/tests/test_user.py b/spyder/config/tests/test_user.py
new file mode 100644
--- /dev/null
+++ b/spyder/config/tests/test_user.py
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+# (see spyder/__init__.py for details)
+
+"""
+Tests for config/user.py
+"""
+
+# Third party imports
+import pytest
+
+# Local imports
+from spyder.config.user import UserConfig
+from spyder.py3compat import PY2
+
+@pytest.fixture
+def userconfig(tmpdir, monkeypatch):
+    monkeypatch.setattr('spyder.config.user.get_conf_path', lambda: str(tmpdir))
+    inifile = tmpdir.join('foo.ini')
+    iniContents = '[main]\nversion = 1.0.0\n\n'
+    if PY2: # strings are quoted in Python2 but not in Python3
+        iniContents += "[section]\noption = 'value'\n\n"
+    else:
+        iniContents += "[section]\noption = value\n\n"
+    inifile.write(iniContents)
+    return UserConfig('foo', defaults={}, subfolder=True,
+                      version='1.0.0', raw_mode=True)
+
+def test_userconfig_get_string_from_inifile(userconfig):
+    assert userconfig.get('section', 'option') == 'value'
+
+def test_userconfig_get_does_not_eval_functions(userconfig):
+    # regression test for issue #3354
+    userconfig.set('section', 'option', 'print("foo")')
+    assert userconfig.get('section', 'option') == 'print("foo")'
+
+def test_userconfig_set_with_string(userconfig):
+    userconfig.set('section', 'option', 'new value')
+    with open(userconfig.filename()) as inifile:
+        iniContents = inifile.read()
+    expected = '[main]\nversion = 1.0.0\n\n'
+    if PY2:
+        expected += "[section]\noption = 'new value'\n\n"
+    else:
+        expected += "[section]\noption = new value\n\n"
+    assert iniContents == expected
+
+
+if __name__ == "__main__":
+    pytest.main()
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
@@ -6,14 +6,13 @@
 """Tests for programs.py"""
 
 import os
-import time
 
 import pytest
 
 from spyder.utils.programs import run_python_script_in_terminal
 
 @pytest.mark.skipif(os.name == 'nt', reason='gets stuck on Windows') # FIXME
-def test_run_python_script_in_terminal(tmpdir):
+def test_run_python_script_in_terminal(tmpdir, qtbot):
     scriptpath = tmpdir.join('write-done.py')
     outfilepath = tmpdir.join('out.txt')
     script = ("with open('out.txt', 'w') as f:\n"
@@ -21,19 +20,19 @@ def test_run_python_script_in_terminal(tmpdir):
     scriptpath.write(script)
     run_python_script_in_terminal(scriptpath.strpath, tmpdir.strpath, '',
                                   False, False, '')
-    time.sleep(1) # wait for script to finish
+    qtbot.wait(1000) # wait for script to finish
     res = outfilepath.read()
     assert res == 'done'
 
 @pytest.mark.skipif(os.name == 'nt', reason='gets stuck on Windows') # FIXME
-def test_run_python_script_in_terminal_with_wdir_empty(tmpdir):
+def test_run_python_script_in_terminal_with_wdir_empty(tmpdir, qtbot):
     scriptpath = tmpdir.join('write-done.py')
     outfilepath = tmpdir.join('out.txt')
     script = ("with open('{}', 'w') as f:\n"
               "    f.write('done')\n").format(outfilepath.strpath)
     scriptpath.write(script)
     run_python_script_in_terminal(scriptpath.strpath, '', '', False, False, '')
-    time.sleep(1) # wait for script to finish
+    qtbot.wait(1000) # wait for script to finish
     res = outfilepath.read()
     assert res == 'done'