From 51bbbb6fab8f15cc94852d7d502714710de5352c 2009-04-21 23:57:57 From: Fernando Perez Date: 2009-04-21 23:57:57 Subject: [PATCH] Ensure that we don't damage the __builtins__ object after %run. This fixes lp:364853. Also added a standalone set of testing.tools utilities that can be used in place of nose.tools to get proper test generators in object oriented tests. --- diff --git a/IPython/Magic.py b/IPython/Magic.py index a6194ca..ecb5f9d 100644 --- a/IPython/Magic.py +++ b/IPython/Magic.py @@ -1709,6 +1709,16 @@ Currently the magic system has the following functions:\n""" del prog_ns['__name__'] self.shell.user_ns.update(prog_ns) finally: + # It's a bit of a mystery why, but __builtins__ can change from + # being a module to becoming a dict missing some key data after + # %run. As best I can see, this is NOT something IPython is doing + # at all, and similar problems have been reported before: + # http://coding.derkeiler.com/Archive/Python/comp.lang.python/2004-10/0188.html + # Since this seems to be done by the interpreter itself, the best + # we can do is to at least restore __builtins__ for the user on + # exit. + self.shell.user_ns['__builtins__'] = __builtin__ + # Ensure key global structures are restored sys.argv = save_argv if restore_main: diff --git a/IPython/testing/plugin/ipdoctest.py b/IPython/testing/plugin/ipdoctest.py index 734afdf..79211de 100644 --- a/IPython/testing/plugin/ipdoctest.py +++ b/IPython/testing/plugin/ipdoctest.py @@ -123,10 +123,19 @@ class ipnsdict(dict): def update(self,other): self._checkpoint() dict.update(self,other) + # If '_' is in the namespace, python won't set it when executing code, # and we have examples that test it. So we ensure that the namespace # is always 'clean' of it before it's used for test code execution. self.pop('_',None) + + # The builtins namespace must *always* be the real __builtin__ module, + # else weird stuff happens. The main ipython code does have provisions + # to ensure this after %run, but since in this class we do some + # aggressive low-level cleaning of the execution namespace, we need to + # correct for that ourselves, to ensure consitency with the 'real' + # ipython. + self['__builtins__'] = __builtin__ def start_ipython(): diff --git a/IPython/testing/plugin/test_ipdoctest.py b/IPython/testing/plugin/test_ipdoctest.py index 6d04b92..f5a6a4e 100644 --- a/IPython/testing/plugin/test_ipdoctest.py +++ b/IPython/testing/plugin/test_ipdoctest.py @@ -18,6 +18,25 @@ def doctest_simple(): """ +def doctest_run_builtins(): + """Check that %run doesn't damage __builtins__ via a doctest. + + This is similar to the test_run_builtins, but I want *both* forms of the + test to catch any possible glitches in our testing machinery, since that + modifies %run somewhat. So for this, we have both a normal test (below) + and a doctest (this one). + + In [1]: import tempfile + + In [3]: f = tempfile.NamedTemporaryFile() + + In [4]: f.write('pass\\n') + + In [5]: f.flush() + + In [7]: %run $f.name + """ + def doctest_multiline1(): """The ipdoctest machinery must handle multiline examples gracefully. diff --git a/IPython/testing/tools.py b/IPython/testing/tools.py new file mode 100644 index 0000000..b9f52f1 --- /dev/null +++ b/IPython/testing/tools.py @@ -0,0 +1,88 @@ +"""Generic testing tools that do NOT depend on Twisted. + +In particular, this module exposes a set of top-level assert* functions that +can be used in place of nose.tools.assert* in method generators (the ones in +nose can not, at least as of nose 0.10.4). + +Note: our testing package contains testing.util, which does depend on Twisted +and provides utilities for tests that manage Deferreds. All testing support +tools that only depend on nose, IPython or the standard library should go here +instead. + + +Authors +------- +- Fernando Perez +""" + +#***************************************************************************** +# Copyright (C) 2009 The IPython Development Team +# +# Distributed under the terms of the BSD License. The full license is in +# the file COPYING, distributed as part of this software. +#***************************************************************************** + +#----------------------------------------------------------------------------- +# Required modules and packages +#----------------------------------------------------------------------------- + +# Standard Python lib +import os +import sys + +# Third-party +import nose.tools as nt + +# From this project +from IPython.tools import utils + +#----------------------------------------------------------------------------- +# Globals +#----------------------------------------------------------------------------- + +# Make a bunch of nose.tools assert wrappers that can be used in test +# generators. This will expose an assert* function for each one in nose.tools. + +_tpl = """ +def %(name)s(*a,**kw): + return nt.%(name)s(*a,**kw) +""" + +for _x in [a for a in dir(nt) if a.startswith('assert')]: + exec _tpl % dict(name=_x) + +#----------------------------------------------------------------------------- +# Functions and classes +#----------------------------------------------------------------------------- + +def full_path(startPath,files): + """Make full paths for all the listed files, based on startPath. + + Only the base part of startPath is kept, since this routine is typically + used with a script's __file__ variable as startPath. The base of startPath + is then prepended to all the listed files, forming the output list. + + :Parameters: + startPath : string + Initial path to use as the base for the results. This path is split + using os.path.split() and only its first component is kept. + + files : string or list + One or more files. + + :Examples: + + >>> full_path('/foo/bar.py',['a.txt','b.txt']) + ['/foo/a.txt', '/foo/b.txt'] + + >>> full_path('/foo',['a.txt','b.txt']) + ['/a.txt', '/b.txt'] + + If a single file is given, the output is still a list: + >>> full_path('/foo','a.txt') + ['/a.txt'] + """ + + files = utils.list_strings(files) + base = os.path.split(startPath)[0] + return [ os.path.join(base,f) for f in files ] diff --git a/IPython/tests/test_magic.py b/IPython/tests/test_magic.py index 7a01728..51aef35 100644 --- a/IPython/tests/test_magic.py +++ b/IPython/tests/test_magic.py @@ -6,12 +6,16 @@ Needs to be run by nose (to make ipython session available). # Standard library imports import os import sys +import tempfile +import types # Third-party imports import nose.tools as nt # From our own code from IPython.testing import decorators as dec +from IPython.testing import tools as tt + #----------------------------------------------------------------------------- # Test functions begin @@ -33,26 +37,6 @@ def test_rehashx(): assert len(scoms) > 10 -def doctest_run_ns(): - """Classes declared %run scripts must be instantiable afterwards. - - In [11]: run tclass foo - - In [12]: isinstance(f(),foo) - Out[12]: True - """ - - -def doctest_run_ns2(): - """Classes declared %run scripts must be instantiable afterwards. - - In [4]: run tclass C-first_pass - - In [5]: run tclass C-second_pass - tclass.py: deleting object: C-first_pass - """ - - def doctest_hist_f(): """Test %hist -f with temporary filename. @@ -63,59 +47,6 @@ def doctest_hist_f(): In [11]: %history -n -f $tfile 3 """ -def doctest_run_builtins(): - """Check that %run doesn't damage __builtins__ via a doctest. - - This is similar to the test_run_builtins, but I want *both* forms of the - test to catch any possible glitches in our testing machinery, since that - modifies %run somewhat. So for this, we have both a normal test (below) - and a doctest (this one). - - In [1]: import tempfile - - In [2]: bid1 = id(__builtins__) - - In [3]: f = tempfile.NamedTemporaryFile() - - In [4]: f.write('pass\\n') - - In [5]: f.flush() - - In [6]: print 'B1:',type(__builtins__) - B1: - - In [7]: %run $f.name - - In [8]: bid2 = id(__builtins__) - - In [9]: print 'B2:',type(__builtins__) - B2: - - In [10]: bid1 == bid2 - Out[10]: True - """ - -def test_run_builtins(): - """Check that %run doesn't damage __builtins__ """ - import sys - import tempfile - import types - - # Make an empty file and put 'pass' in it - f = tempfile.NamedTemporaryFile() - f.write('pass\n') - f.flush() - - # Our first test is that the id of __builtins__ is not modified by %run - bid1 = id(__builtins__) - _ip.magic('run %s' % f.name) - bid2 = id(__builtins__) - yield nt.assert_equals,bid1,bid2 - # However, the above could pass if __builtins__ was already modified to be - # a dict (it should be a module) by a previous use of %run. So we also - # check explicitly that it really is a module: - yield nt.assert_equals,type(__builtins__),type(sys) - def doctest_hist_r(): """Test %hist -r @@ -201,3 +132,104 @@ def doctest_refbug(): lowercased: hello lowercased: hello """ + +#----------------------------------------------------------------------------- +# Tests for %run +#----------------------------------------------------------------------------- + +# %run is critical enough that it's a good idea to have a solid collection of +# tests for it, some as doctests and some as normal tests. + +def doctest_run_ns(): + """Classes declared %run scripts must be instantiable afterwards. + + In [11]: run tclass foo + + In [12]: isinstance(f(),foo) + Out[12]: True + """ + + +def doctest_run_ns2(): + """Classes declared %run scripts must be instantiable afterwards. + + In [4]: run tclass C-first_pass + + In [5]: run tclass C-second_pass + tclass.py: deleting object: C-first_pass + """ + +def doctest_run_builtins(): + """Check that %run doesn't damage __builtins__ via a doctest. + + This is similar to the test_run_builtins, but I want *both* forms of the + test to catch any possible glitches in our testing machinery, since that + modifies %run somewhat. So for this, we have both a normal test (below) + and a doctest (this one). + + In [1]: import tempfile + + In [2]: bid1 = id(__builtins__) + + In [3]: f = tempfile.NamedTemporaryFile() + + In [4]: f.write('pass\\n') + + In [5]: f.flush() + + In [6]: print 'B1:',type(__builtins__) + B1: + + In [7]: %run $f.name + + In [8]: bid2 = id(__builtins__) + + In [9]: print 'B2:',type(__builtins__) + B2: + + In [10]: bid1 == bid2 + Out[10]: True + """ + +# For some tests, it will be handy to organize them in a class with a common +# setup that makes a temp file + +class TestMagicRun(object): + + def setup(self): + """Make a valid python temp file.""" + f = tempfile.NamedTemporaryFile() + f.write('pass\n') + f.flush() + self.tmpfile = f + + def run_tmpfile(self): + _ip.magic('run %s' % self.tmpfile.name) + + def test_builtins_id(self): + """Check that %run doesn't damage __builtins__ """ + + # Test that the id of __builtins__ is not modified by %run + bid1 = id(_ip.user_ns['__builtins__']) + self.run_tmpfile() + bid2 = id(_ip.user_ns['__builtins__']) + tt.assert_equals(bid1, bid2) + + def test_builtins_type(self): + """Check that the type of __builtins__ doesn't change with %run. + + However, the above could pass if __builtins__ was already modified to + be a dict (it should be a module) by a previous use of %run. So we + also check explicitly that it really is a module: + """ + self.run_tmpfile() + tt.assert_equals(type(_ip.user_ns['__builtins__']),type(sys)) + + def test_prompts(self): + """Test that prompts correctly generate after %run""" + self.run_tmpfile() + p2 = str(_ip.IP.outputcache.prompt2).strip() + nt.assert_equals(p2[:3], '...') + + def teardown(self): + self.tmpfile.close()