From 527113ce115d8ab2975204368993a17c09d86baa 2008-08-14 12:48:46 From: Fernando Perez Date: 2008-08-14 12:48:46 Subject: [PATCH] Correctly implement namespace handling. Minor cleanups. Finally found a solution to manage the namespace where doctests are run in conjunction with the IPython one. All tests seem to pass now (except for an intermittent failure in one Twisted one that looks like a race condition somewhere, since it doesn't always appear). --- diff --git a/IPython/testing/plugin/Makefile b/IPython/testing/plugin/Makefile index da08e17..878946c 100644 --- a/IPython/testing/plugin/Makefile +++ b/IPython/testing/plugin/Makefile @@ -3,7 +3,8 @@ PREFIX=~/usr/local PREFIX=~/tmp/local NOSE0=nosetests -vs --with-doctest --doctest-tests --detailed-errors -NOSE=nosetests -vvs --with-ipdoctest --doctest-tests --doctest-extension=txt --detailed-errors +NOSE=nosetests -vvs --with-ipdoctest --doctest-tests --doctest-extension=txt \ +--detailed-errors SRC=ipdoctest.py setup.py ../decorators.py @@ -31,30 +32,38 @@ deb: plugin dtexample.py # IPython tests deco: - $(NOSE0) -x IPython.testing.decorators + $(NOSE0) IPython.testing.decorators magic: plugin - $(NOSE) -x IPython.Magic + $(NOSE) IPython.Magic ipipe: plugin - $(NOSE) -x IPython.Extensions.ipipe + $(NOSE) IPython.Extensions.ipipe iplib: plugin - $(NOSE) -x IPython.iplib + $(NOSE) IPython.iplib strd: plugin - nosetests -vs --with-doctest --doctest-tests IPython.strdispatch $(NOSE) IPython.strdispatch +engine: plugin + $(NOSE) IPython.kernel + +tf: plugin + $(NOSE) IPython.config.traitlets + # All of ipython itself ipython: plugin $(NOSE) IPython + # Combined targets sr: rtest strd base: dtest rtest test strd deco +quick: base iplib ipipe + all: base ipython # Main plugin and cleanup diff --git a/IPython/testing/plugin/ipdoctest.py b/IPython/testing/plugin/ipdoctest.py index bfd6325..5273781 100644 --- a/IPython/testing/plugin/ipdoctest.py +++ b/IPython/testing/plugin/ipdoctest.py @@ -13,21 +13,6 @@ Limitations: '_34==True', for example). For IPython tests run via an external process the prompt numbers may be different, and IPython tests run as normal python code won't even have these special _NN variables set at all. - -- IPython functions that produce output as a side-effect of calling a system - process (e.g. 'ls') can be doc-tested, but they must be handled in an - external IPython process. Such doctests must be tagged with: - - # ipdoctest: EXTERNAL - - so that the testing machinery handles them differently. Since these are run - via pexpect in an external process, they can't deal with exceptions or other - fancy featurs of regular doctests. You must limit such tests to simple - matching of the output. For this reason, I recommend you limit these kinds - of doctests to features that truly require a separate process, and use the - normal IPython ones (which have all the features of normal doctests) for - everything else. See the examples at the bottom of this file for a - comparison of what can be done with both types. """ @@ -63,9 +48,6 @@ import nose.core from nose.plugins import doctests, Plugin from nose.util import anyp, getpackage, test_address, resolve_name, tolist -# Our own imports -#from extdoctest import ExtensionDoctest, DocTestFinder -#from dttools import DocTestFinder, DocTestCase #----------------------------------------------------------------------------- # Module globals and other constants @@ -79,9 +61,9 @@ log = logging.getLogger(__name__) # gets the job done. -# XXX - Hack to modify the %run command so we can sync the user's namespace -# with the test globals. Once we move over to a clean magic system, this will -# be done with much less ugliness. +# Hack to modify the %run command so we can sync the user's namespace with the +# test globals. Once we move over to a clean magic system, this will be done +# with much less ugliness. def _run_ns_sync(self,arg_s,runner=None): """Modified version of %run that syncs testing namespaces. @@ -94,27 +76,34 @@ def _run_ns_sync(self,arg_s,runner=None): return out -# XXX1 - namespace handling -class ncdict(dict): +class ipnsdict(dict): + """A special subclass of dict for use as an IPython namespace in doctests. + + This subclass adds a simple checkpointing capability so that when testing + machinery clears it (we use it as the test execution context), it doesn't + get completely destroyed. + """ + def __init__(self,*a): dict.__init__(self,*a) self._savedict = {} - def copy(self): - return self - def clear(self): - import IPython - - print 'NCDICT - clear' # dbg dict.clear(self) - self.update(IPython.ipapi.make_user_ns()) self.update(self._savedict) - def remember(self,adict): - self._savedict = adict - -#class ncdict(dict): pass + def _checkpoint(self): + self._savedict.clear() + self._savedict.update(self) + + 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) + def start_ipython(): """Start a global IPython shell, which we need for IPython-specific syntax. @@ -139,7 +128,7 @@ def start_ipython(): _main = sys.modules.get('__main__') # Start IPython instance. We customize it to start with minimal frills. - user_ns,global_ns = IPython.ipapi.make_user_namespaces(ncdict(),dict()) + user_ns,global_ns = IPython.ipapi.make_user_namespaces(ipnsdict(),dict()) IPython.Shell.IPShell(['--classic','--noterm_title'], user_ns,global_ns) @@ -275,85 +264,6 @@ class DocTestFinder(doctest.DocTestFinder): globs, seen) - # XXX1 - namespace handling - def Xfind(self, obj, name=None, module=None, globs=None, extraglobs=None): - """ - Return a list of the DocTests that are defined by the given - object's docstring, or by any of its contained objects' - docstrings. - - The optional parameter `module` is the module that contains - the given object. If the module is not specified or is None, then - the test finder will attempt to automatically determine the - correct module. The object's module is used: - - - As a default namespace, if `globs` is not specified. - - To prevent the DocTestFinder from extracting DocTests - from objects that are imported from other modules. - - To find the name of the file containing the object. - - To help find the line number of the object within its - file. - - Contained objects whose module does not match `module` are ignored. - - If `module` is False, no attempt to find the module will be made. - This is obscure, of use mostly in tests: if `module` is False, or - is None but cannot be found automatically, then all objects are - considered to belong to the (non-existent) module, so all contained - objects will (recursively) be searched for doctests. - - The globals for each DocTest is formed by combining `globs` - and `extraglobs` (bindings in `extraglobs` override bindings - in `globs`). A new copy of the globals dictionary is created - for each DocTest. If `globs` is not specified, then it - defaults to the module's `__dict__`, if specified, or {} - otherwise. If `extraglobs` is not specified, then it defaults - to {}. - - """ - - # Find the module that contains the given object (if obj is - # a module, then module=obj.). Note: this may fail, in which - # case module will be None. - if module is False: - module = None - elif module is None: - module = inspect.getmodule(obj) - - # always build our own globals - if globs is None: - if module is None: - globs = {} - else: - globs = module.__dict__.copy() - else: - globs.update(module.__dict__.copy()) - - print 'globs is:',globs.keys() - - if extraglobs is not None: - globs.update(extraglobs) - - try: - globs.remember(module.__dict__) - except: - pass - - ## # Initialize globals, and merge in extraglobs. - ## if globs is None: - ## if module is None: - ## globs = {} - ## else: - ## globs = module.__dict__.copy() - ## else: - ## globs = globs.copy() - ## if extraglobs is not None: - ## globs.update(extraglobs) - - return doctest.DocTestFinder.find(self,obj,name,module,globs, - extraglobs) - - class IPDoctestOutputChecker(doctest.OutputChecker): """Second-chance checker with support for random tests. @@ -411,12 +321,20 @@ class DocTestCase(doctests.DocTestCase): self._dt_setUp = setUp self._dt_tearDown = tearDown + # XXX - store this runner once in the object! + runner = IPDocTestRunner(optionflags=optionflags, + checker=checker, verbose=False) + self._dt_runner = runner + + # Each doctest should remember what directory it was loaded from... self._ori_dir = os.getcwd() # Modified runTest from the default stdlib def runTest(self): test = self._dt_test + runner = self._dt_runner + old = sys.stdout new = StringIO() optionflags = self._dt_optionflags @@ -426,9 +344,6 @@ class DocTestCase(doctests.DocTestCase): # so add the default reporting flags optionflags |= _unittest_reportflags - runner = IPDocTestRunner(optionflags=optionflags, - checker=self._dt_checker, verbose=False) - try: # Save our current directory and switch out to the one where the # test was originally created, in case another doctest did a @@ -437,8 +352,8 @@ class DocTestCase(doctests.DocTestCase): os.chdir(self._ori_dir) runner.DIVIDER = "-"*70 - failures, tries = runner.run( - test, out=new.write, clear_globs=False) + failures, tries = runner.run(test,out=new.write, + clear_globs=False) finally: sys.stdout = old os.chdir(curdir) @@ -446,11 +361,18 @@ class DocTestCase(doctests.DocTestCase): if failures: raise self.failureException(self.format_failure(new.getvalue())) - # XXX1 - namespace handling - def XtearDown(self): - print '!! teardown!' # dbg - doctests.DocTestCase.tearDown(self) - + def setUp(self): + """Modified test setup that syncs with ipython namespace""" + + if isinstance(self._dt_test.examples[0],IPExample): + # for IPython examples *only*, we swap the globals with the ipython + # namespace, after updating it with the globals (which doctest + # fills with the necessary info from the module being tested). + _ip.IP.user_ns.update(self._dt_test.globs) + self._dt_test.globs = _ip.IP.user_ns + + doctests.DocTestCase.setUp(self) + # A simple subclassing of the original with a different class name, so we can @@ -548,6 +470,8 @@ class IPDocTestParser(doctest.DocTestParser): output = [] charno, lineno = 0, 0 + # We make 'all random' tests by adding the '# random' mark to every + # block of output in the test. if self._RANDOM_TEST.search(string): random_marker = '\n# random' else: @@ -715,12 +639,6 @@ class IPDocTestRunner(doctest.DocTestRunner,object): # for all examples, most of which won't be calling %run anyway). _run_ns_sync.test_globs = test.globs - # dbg - ## print >> sys.stderr, "Test:",test - ## for ex in test.examples: - ## print >> sys.stderr, ex.source - ## print >> sys.stderr, 'Want:\n',ex.want,'\n--' - return super(IPDocTestRunner,self).run(test, compileflags,out,clear_globs) @@ -745,8 +663,10 @@ class ExtensionDoctest(doctests.Doctest): Plugin.configure(self, options, config) self.doctest_tests = options.doctest_tests self.extension = tolist(options.doctestExtension) - self.finder = DocTestFinder() + self.parser = doctest.DocTestParser() + self.finder = DocTestFinder() + self.checker = IPDoctestOutputChecker() self.globs = None self.extraglobs = None @@ -776,6 +696,9 @@ class ExtensionDoctest(doctests.Doctest): if not tests: return + # always use whitespace and ellipsis options + optionflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS + tests.sort() module_file = module.__file__ if module_file[-4:] in ('.pyc', '.pyo'): @@ -785,15 +708,11 @@ class ExtensionDoctest(doctests.Doctest): continue if not test.filename: test.filename = module_file - - # xxx - checker and options may be ok instantiated once outside loop - # always use whitespace and ellipsis options - optionflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS - checker = IPDoctestOutputChecker() yield DocTestCase(test, optionflags=optionflags, - checker=checker) + checker=self.checker) + def loadTestsFromFile(self, filename): #print 'lTF',filename # dbg @@ -826,7 +745,7 @@ class ExtensionDoctest(doctests.Doctest): """ #print 'Filename:',filename # dbg - # temporarily hardcoded list, will move to driver later + # XXX - temporarily hardcoded list, will move to driver later exclude = ['IPython/external/', 'IPython/Extensions/ipy_', 'IPython/platutils_win32', @@ -857,11 +776,9 @@ class IPythonDoctest(ExtensionDoctest): Plugin.configure(self, options, config) self.doctest_tests = options.doctest_tests self.extension = tolist(options.doctestExtension) + self.parser = IPDocTestParser() self.finder = DocTestFinder(parser=self.parser) - - # XXX1 - namespace handling + self.checker = IPDoctestOutputChecker() self.globs = None - #self.globs = _ip.IP.user_ns - self.extraglobs = None