diff --git a/IPython/testing/plugin/Makefile b/IPython/testing/plugin/Makefile index 0f34a1a..106933d 100644 --- a/IPython/testing/plugin/Makefile +++ b/IPython/testing/plugin/Makefile @@ -2,29 +2,45 @@ PREFIX=~/usr/local PREFIX=~/tmp/local +NOSE0=nosetests -vs --with-doctest --doctest-tests +NOSE=nosetests -vvs --with-ipdoctest --doctest-tests --doctest-extension=txt + +#--with-color + +SRC=ipdoctest.py setup.py decorators.py + plugin: IPython_doctest_plugin.egg-info dtest: plugin dtexample.py - nosetests -vs --with-ipdoctest --doctest-tests --doctest-extension=txt \ - dtexample.py + $(NOSE) dtexample.py # Note: this test is double counting!!! rtest: plugin dtexample.py - nosetests -vs --with-ipdoctest --doctest-tests test_refs.py + $(NOSE) test_refs.py + +std: plugin + nosetests -vs --with-doctest --doctest-tests IPython.strdispatch + $(NOSE) IPython.strdispatch test: plugin dtexample.py - nosetests -vs --with-ipdoctest --doctest-tests --doctest-extension=txt \ - dtexample.py test*.py test*.txt + $(NOSE) dtexample.py test*.py test*.txt deb: plugin dtexample.py - nosetests -vs --with-ipdoctest --doctest-tests --doctest-extension=txt \ - test_combo.txt + $(NOSE) test_combo.txt iptest: plugin - nosetests -vs --with-ipdoctest --doctest-tests --doctest-extension=txt \ - IPython + $(NOSE) IPython + +deco: + $(NOSE0) decorators.py + +sr: rtest std + +base: dtest rtest test std deco + +all: base iptest -IPython_doctest_plugin.egg-info: ipdoctest.py setup.py +IPython_doctest_plugin.egg-info: $(SRC) python setup.py install --prefix=$(PREFIX) touch $@ diff --git a/IPython/testing/plugin/decorator_msim.py b/IPython/testing/plugin/decorator_msim.py new file mode 100644 index 0000000..8915cf0 --- /dev/null +++ b/IPython/testing/plugin/decorator_msim.py @@ -0,0 +1,146 @@ +## The basic trick is to generate the source code for the decorated function +## with the right signature and to evaluate it. +## Uncomment the statement 'print >> sys.stderr, func_src' in _decorate +## to understand what is going on. + +__all__ = ["decorator", "update_wrapper", "getinfo"] + +import inspect, sys + +def getinfo(func): + """ + Returns an info dictionary containing: + - name (the name of the function : str) + - argnames (the names of the arguments : list) + - defaults (the values of the default arguments : tuple) + - signature (the signature : str) + - doc (the docstring : str) + - module (the module name : str) + - dict (the function __dict__ : str) + + >>> def f(self, x=1, y=2, *args, **kw): pass + + >>> info = getinfo(f) + + >>> info["name"] + 'f' + >>> info["argnames"] + ['self', 'x', 'y', 'args', 'kw'] + + >>> info["defaults"] + (1, 2) + + >>> info["signature"] + 'self, x, y, *args, **kw' + """ + assert inspect.ismethod(func) or inspect.isfunction(func) + regargs, varargs, varkwargs, defaults = inspect.getargspec(func) + argnames = list(regargs) + if varargs: + argnames.append(varargs) + if varkwargs: + argnames.append(varkwargs) + signature = inspect.formatargspec(regargs, varargs, varkwargs, defaults, + formatvalue=lambda value: "")[1:-1] + return dict(name=func.__name__, argnames=argnames, signature=signature, + defaults = func.func_defaults, doc=func.__doc__, + module=func.__module__, dict=func.__dict__, + globals=func.func_globals, closure=func.func_closure) + +def update_wrapper(wrapper, wrapped, create=False): + """ + An improvement over functools.update_wrapper. By default it works the + same, but if the 'create' flag is set, generates a copy of the wrapper + with the right signature and update the copy, not the original. + Moreovoer, 'wrapped' can be a dictionary with keys 'name', 'doc', 'module', + 'dict', 'defaults'. + """ + if isinstance(wrapped, dict): + infodict = wrapped + else: # assume wrapped is a function + infodict = getinfo(wrapped) + assert not '_wrapper_' in infodict["argnames"], \ + '"_wrapper_" is a reserved argument name!' + if create: # create a brand new wrapper with the right signature + src = "lambda %(signature)s: _wrapper_(%(signature)s)" % infodict + # import sys; print >> sys.stderr, src # for debugging purposes + wrapper = eval(src, dict(_wrapper_=wrapper)) + try: + wrapper.__name__ = infodict['name'] + except: # Python version < 2.4 + pass + wrapper.__doc__ = infodict['doc'] + wrapper.__module__ = infodict['module'] + wrapper.__dict__.update(infodict['dict']) + wrapper.func_defaults = infodict['defaults'] + return wrapper + +# the real meat is here +def _decorator(caller, func): + infodict = getinfo(func) + argnames = infodict['argnames'] + assert not ('_call_' in argnames or '_func_' in argnames), \ + 'You cannot use _call_ or _func_ as argument names!' + src = "lambda %(signature)s: _call_(_func_, %(signature)s)" % infodict + dec_func = eval(src, dict(_func_=func, _call_=caller)) + return update_wrapper(dec_func, func) + +def decorator(caller, func=None): + """ + General purpose decorator factory: takes a caller function as + input and returns a decorator with the same attributes. + A caller function is any function like this:: + + def caller(func, *args, **kw): + # do something + return func(*args, **kw) + + Here is an example of usage: + + >>> @decorator + ... def chatty(f, *args, **kw): + ... print "Calling %r" % f.__name__ + ... return f(*args, **kw) + + >>> chatty.__name__ + 'chatty' + + >>> @chatty + ... def f(): pass + ... + >>> f() + Calling 'f' + + For sake of convenience, the decorator factory can also be called with + two arguments. In this casem ``decorator(caller, func)`` is just a + shortcut for ``decorator(caller)(func)``. + """ + if func is None: # return a decorator function + return update_wrapper(lambda f : _decorator(caller, f), caller) + else: # return a decorated function + return _decorator(caller, func) + +if __name__ == "__main__": + import doctest; doctest.testmod() + +####################### LEGALESE ################################## + +## Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## Redistributions in bytecode form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in +## the documentation and/or other materials provided with the +## distribution. + +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +## HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +## INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +## BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +## OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +## ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +## TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +## USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +## DAMAGE. diff --git a/IPython/testing/plugin/decorators.py b/IPython/testing/plugin/decorators.py new file mode 100644 index 0000000..86266c7 --- /dev/null +++ b/IPython/testing/plugin/decorators.py @@ -0,0 +1,144 @@ +"""Decorators for labeling test objects. + +Decorators that merely return a modified version of the original +function object are straightforward. Decorators that return a new +function object need to use +nose.tools.make_decorator(original_function)(decorator) in returning +the decorator, in order to preserve metadata such as function name, +setup and teardown functions and so on - see nose.tools for more +information. + +NOTE: This file contains IPython-specific decorators and imports the +numpy.testing.decorators file, which we've copied verbatim. Any of our own +code will be added at the bottom if we end up extending this. +""" + +# Stdlib imports +import inspect + +# Third-party imports + +# This is Michele Simionato's decorator module, also kept verbatim. +from decorator_msim import decorator + +# Grab the numpy-specific decorators which we keep in a file that we +# occasionally update from upstream: decorators_numpy.py is an IDENTICAL copy +# of numpy.testing.decorators. +from decorators_numpy import * + +############################################################################## +# Local code begins + +# Utility functions + +def apply_wrapper(wrapper,func): + """Apply a wrapper to a function for decoration. + + This mixes Michele Simionato's decorator tool with nose's make_decorator, + to apply a wrapper in a decorator so that all nose attributes, as well as + function signature and other properties, survive the decoration cleanly. + This will ensure that wrapped functions can still be well introspected via + IPython, for example. + """ + import nose.tools + + return decorator(wrapper,nose.tools.make_decorator(func)(wrapper)) + + +def make_label_dec(label,ds=None): + """Factory function to create a decorator that applies one or more labels. + + :Parameters: + label : string or sequence + One or more labels that will be applied by the decorator to the functions + it decorates. Labels are attributes of the decorated function with their + value set to True. + + :Keywords: + ds : string + An optional docstring for the resulting decorator. If not given, a + default docstring is auto-generated. + + :Returns: + A decorator. + + :Examples: + + A simple labeling decorator: + >>> slow = make_label_dec('slow') + >>> print slow.__doc__ + Labels a test as 'slow'. + + And one that uses multiple labels and a custom docstring: + >>> rare = make_label_dec(['slow','hard'], + ... "Mix labels 'slow' and 'hard' for rare tests.") + >>> print rare.__doc__ + Mix labels 'slow' and 'hard' for rare tests. + + Now, let's test using this one: + >>> @rare + ... def f(): pass + ... + >>> + >>> f.slow + True + >>> f.hard + True + """ + + if isinstance(label,basestring): + labels = [label] + else: + labels = label + + # Validate that the given label(s) are OK for use in setattr() by doing a + # dry run on a dummy function. + tmp = lambda : None + for label in labels: + setattr(tmp,label,True) + + # This is the actual decorator we'll return + def decor(f): + for label in labels: + setattr(f,label,True) + return f + + # Apply the user's docstring, or autogenerate a basic one + if ds is None: + ds = "Labels a test as %r." % label + decor.__doc__ = ds + + return decor + +#----------------------------------------------------------------------------- +# Decorators for public use + +def skip_doctest(func): + """Decorator - mark a function for skipping its doctest. + + This decorator allows you to mark a function whose docstring you wish to + omit from testing, while preserving the docstring for introspection, help, + etc.""" + + # We just return the function unmodified, but the wrapping has the effect + # of making the doctest plugin skip the doctest. + def wrapper(*a,**k): + return func(*a,**k) + + # Here we use plain 'decorator' and not apply_wrapper, because we don't + # need all the nose-protection machinery (functions containing doctests + # can't be full-blown nose tests, so we don't need to prserve + # setup/teardown). + return decorator(wrapper,func) + + +def skip(func): + """Decorator - mark a test function for skipping from test suite.""" + + import nose + + def wrapper(*a,**k): + raise nose.SkipTest("Skipping test for function: %s" % + func.__name__) + + return apply_wrapper(wrapper,func) diff --git a/IPython/testing/plugin/decorators_numpy.py b/IPython/testing/plugin/decorators_numpy.py new file mode 100644 index 0000000..dd9783e --- /dev/null +++ b/IPython/testing/plugin/decorators_numpy.py @@ -0,0 +1,94 @@ +"""Decorators for labeling test objects + +Decorators that merely return a modified version of the original +function object are straightforward. Decorators that return a new +function object need to use +nose.tools.make_decorator(original_function)(decorator) in returning +the decorator, in order to preserve metadata such as function name, +setup and teardown functions and so on - see nose.tools for more +information. + +""" + +def slow(t): + """Labels a test as 'slow'. + + The exact definition of a slow test is obviously both subjective and + hardware-dependent, but in general any individual test that requires more + than a second or two should be labeled as slow (the whole suite consits of + thousands of tests, so even a second is significant).""" + + t.slow = True + return t + +def setastest(tf=True): + ''' Signals to nose that this function is or is not a test + + Parameters + ---------- + tf : bool + If True specifies this is a test, not a test otherwise + + e.g + >>> from numpy.testing.decorators import setastest + >>> @setastest(False) + ... def func_with_test_in_name(arg1, arg2): pass + ... + >>> + + This decorator cannot use the nose namespace, because it can be + called from a non-test module. See also istest and nottest in + nose.tools + + ''' + def set_test(t): + t.__test__ = tf + return t + return set_test + +def skipif(skip_condition, msg=None): + ''' Make function raise SkipTest exception if skip_condition is true + + Parameters + --------- + skip_condition : bool + Flag to determine whether to skip test (True) or not (False) + msg : string + Message to give on raising a SkipTest exception + + Returns + ------- + decorator : function + Decorator, which, when applied to a function, causes SkipTest + to be raised when the skip_condition was True, and the function + to be called normally otherwise. + + Notes + ----- + You will see from the code that we had to further decorate the + decorator with the nose.tools.make_decorator function in order to + transmit function name, and various other metadata. + ''' + if msg is None: + msg = 'Test skipped due to test condition' + def skip_decorator(f): + # Local import to avoid a hard nose dependency and only incur the + # import time overhead at actual test-time. + import nose + def skipper(*args, **kwargs): + if skip_condition: + raise nose.SkipTest, msg + else: + return f(*args, **kwargs) + return nose.tools.make_decorator(f)(skipper) + return skip_decorator + +def skipknownfailure(f): + ''' Decorator to raise SkipTest for test known to fail + ''' + # Local import to avoid a hard nose dependency and only incur the + # import time overhead at actual test-time. + import nose + def skipper(*args, **kwargs): + raise nose.SkipTest, 'This test is known to fail' + return nose.tools.make_decorator(f)(skipper) diff --git a/IPython/testing/plugin/ipdoctest.py b/IPython/testing/plugin/ipdoctest.py index 538c7f8..cd42477 100644 --- a/IPython/testing/plugin/ipdoctest.py +++ b/IPython/testing/plugin/ipdoctest.py @@ -68,6 +68,29 @@ log = logging.getLogger(__name__) # machinery into a fit. This code should be considered a gross hack, but it # gets the job done. +class ncdict(dict): + """Non-copying dict class. + + This is a special-purpose dict subclass that overrides the .copy() method + to return the original object itself. We need it to ensure that doctests + happen in the IPython namespace, but doctest always makes a shallow copy of + the given globals for execution. Since we actually *want* this namespace + to be persistent (this is how the user's session maintains state), we + simply fool doctest by returning the original object upoon copy. + """ + + def copy(self): + return self + + +def _my_run(self,arg_s,runner=None): + """ + """ + #print 'HA!' # dbg + + return _ip.IP.magic_run_ori(arg_s,runner) + + def start_ipython(): """Start a global IPython shell, which we need for IPython-specific syntax. """ @@ -88,8 +111,11 @@ def start_ipython(): _excepthook = sys.excepthook _main = sys.modules.get('__main__') - # Start IPython instance - IPython.Shell.IPShell(['--classic','--noterm_title']) + # Start IPython instance. We customize it to start with minimal frills and + # with our own namespace. + argv = ['--classic','--noterm_title'] + user_ns = ncdict() + IPython.Shell.IPShell(argv,user_ns) # Deactivate the various python system hooks added by ipython for # interactive convenience so we don't confuse the doctest system @@ -107,6 +133,11 @@ def start_ipython(): # doctest machinery would miss them. _ip.system = xsys + import new + im = new.instancemethod(_my_run,_ip.IP, _ip.IP.__class__) + _ip.IP.magic_run_ori = _ip.IP.magic_run + _ip.IP.magic_run = im + # The start call MUST be made here. I'm not sure yet why it doesn't work if # it is made later, at plugin initialization time, but in all my tests, that's # the case. @@ -214,14 +245,14 @@ class DocTestFinder(doctest.DocTestFinder): globs, seen) -# second-chance checker; if the default comparison doesn't +# second-chance checker; if the default comparison doesn't # pass, then see if the expected output string contains flags that # tell us to ignore the output class IPDoctestOutputChecker(doctest.OutputChecker): def check_output(self, want, got, optionflags): #print '*** My Checker!' # dbg - - ret = doctest.OutputChecker.check_output(self, want, got, + + ret = doctest.OutputChecker.check_output(self, want, got, optionflags) if not ret: if "#random" in want: @@ -239,22 +270,22 @@ class DocTestCase(doctests.DocTestCase): """ # Note: this method was taken from numpy's nosetester module. - - # Subclass nose.plugins.doctests.DocTestCase to work around a bug in + + # Subclass nose.plugins.doctests.DocTestCase to work around a bug in # its constructor that blocks non-default arguments from being passed # down into doctest.DocTestCase def __init__(self, test, optionflags=0, setUp=None, tearDown=None, checker=None, obj=None, result_var='_'): self._result_var = result_var - doctests.DocTestCase.__init__(self, test, + doctests.DocTestCase.__init__(self, test, optionflags=optionflags, - setUp=setUp, tearDown=tearDown, + setUp=setUp, tearDown=tearDown, checker=checker) # Now we must actually copy the original constructor from the stdlib # doctest class, because we can't call it directly and a bug in nose # means it never gets passed the right arguments. - + self._dt_optionflags = optionflags self._dt_checker = checker self._dt_test = test @@ -325,9 +356,9 @@ class IPDocTestParser(doctest.DocTestParser): out = [] newline = out.append for lnum,line in enumerate(source.splitlines()): - #newline(_ip.IPipython.prefilter(line,True)) newline(_ip.IP.prefilter(line,lnum>0)) newline('') # ensure a closing newline, needed by doctest + #print "PYSRC:", '\n'.join(out) # dbg return '\n'.join(out) def parse(self, string, name=''): @@ -338,7 +369,7 @@ class IPDocTestParser(doctest.DocTestParser): argument `name` is a name identifying this string, and is only used for error messages. """ - + #print 'Parse string:\n',string # dbg string = string.expandtabs() @@ -492,6 +523,201 @@ class IPDocTestParser(doctest.DocTestParser): SKIP = doctest.register_optionflag('SKIP') +class IPDocTestRunner(doctest.DocTestRunner): + + # Unfortunately, doctest uses a private method (__run) for the actual run + # execution, so we can't cleanly override just that part. Instead, we have + # to copy/paste the entire run() implementation so we can call our own + # customized runner. + #///////////////////////////////////////////////////////////////// + # DocTest Running + #///////////////////////////////////////////////////////////////// + + def __run(self, test, compileflags, out): + """ + Run the examples in `test`. Write the outcome of each example + with one of the `DocTestRunner.report_*` methods, using the + writer function `out`. `compileflags` is the set of compiler + flags that should be used to execute examples. Return a tuple + `(f, t)`, where `t` is the number of examples tried, and `f` + is the number of examples that failed. The examples are run + in the namespace `test.globs`. + """ + # Keep track of the number of failures and tries. + failures = tries = 0 + + # Save the option flags (since option directives can be used + # to modify them). + original_optionflags = self.optionflags + + SUCCESS, FAILURE, BOOM = range(3) # `outcome` state + + check = self._checker.check_output + + # Process each example. + for examplenum, example in enumerate(test.examples): + + # If REPORT_ONLY_FIRST_FAILURE is set, then supress + # reporting after the first failure. + quiet = (self.optionflags & REPORT_ONLY_FIRST_FAILURE and + failures > 0) + + # Merge in the example's options. + self.optionflags = original_optionflags + if example.options: + for (optionflag, val) in example.options.items(): + if val: + self.optionflags |= optionflag + else: + self.optionflags &= ~optionflag + + # If 'SKIP' is set, then skip this example. + if self.optionflags & SKIP: + continue + + # Record that we started this example. + tries += 1 + if not quiet: + self.report_start(out, test, example) + + # Use a special filename for compile(), so we can retrieve + # the source code during interactive debugging (see + # __patched_linecache_getlines). + filename = '' % (test.name, examplenum) + + # Run the example in the given context (globs), and record + # any exception that gets raised. (But don't intercept + # keyboard interrupts.) + try: + # Don't blink! This is where the user's code gets run. + exec compile(example.source, filename, "single", + compileflags, 1) in test.globs + self.debugger.set_continue() # ==== Example Finished ==== + exception = None + except KeyboardInterrupt: + raise + except: + exception = sys.exc_info() + self.debugger.set_continue() # ==== Example Finished ==== + + got = self._fakeout.getvalue() # the actual output + self._fakeout.truncate(0) + outcome = FAILURE # guilty until proved innocent or insane + + # If the example executed without raising any exceptions, + # verify its output. + if exception is None: + if check(example.want, got, self.optionflags): + outcome = SUCCESS + + # The example raised an exception: check if it was expected. + else: + exc_info = sys.exc_info() + exc_msg = traceback.format_exception_only(*exc_info[:2])[-1] + if not quiet: + got += _exception_traceback(exc_info) + + # If `example.exc_msg` is None, then we weren't expecting + # an exception. + if example.exc_msg is None: + outcome = BOOM + + # We expected an exception: see whether it matches. + elif check(example.exc_msg, exc_msg, self.optionflags): + outcome = SUCCESS + + # Another chance if they didn't care about the detail. + elif self.optionflags & IGNORE_EXCEPTION_DETAIL: + m1 = re.match(r'[^:]*:', example.exc_msg) + m2 = re.match(r'[^:]*:', exc_msg) + if m1 and m2 and check(m1.group(0), m2.group(0), + self.optionflags): + outcome = SUCCESS + + # Report the outcome. + if outcome is SUCCESS: + if not quiet: + self.report_success(out, test, example, got) + elif outcome is FAILURE: + if not quiet: + self.report_failure(out, test, example, got) + failures += 1 + elif outcome is BOOM: + if not quiet: + self.report_unexpected_exception(out, test, example, + exc_info) + failures += 1 + else: + assert False, ("unknown outcome", outcome) + + # Restore the option flags (in case they were modified) + self.optionflags = original_optionflags + + # Record and return the number of failures and tries. + + #self.__record_outcome(test, failures, tries) + + # Hack to access a parent private method by working around Python's + # name mangling (which is fortunately simple). + doctest.DocTestRunner._DocTestRunner__record_outcome(self,test, + failures, tries) + return failures, tries + + def run(self, test, compileflags=None, out=None, clear_globs=True): + """ + Run the examples in `test`, and display the results using the + writer function `out`. + + The examples are run in the namespace `test.globs`. If + `clear_globs` is true (the default), then this namespace will + be cleared after the test runs, to help with garbage + collection. If you would like to examine the namespace after + the test completes, then use `clear_globs=False`. + + `compileflags` gives the set of flags that should be used by + the Python compiler when running the examples. If not + specified, then it will default to the set of future-import + flags that apply to `globs`. + + The output of each example is checked using + `DocTestRunner.check_output`, and the results are formatted by + the `DocTestRunner.report_*` methods. + """ + self.test = test + + if compileflags is None: + compileflags = _extract_future_flags(test.globs) + + save_stdout = sys.stdout + if out is None: + out = save_stdout.write + sys.stdout = self._fakeout + + # Patch pdb.set_trace to restore sys.stdout during interactive + # debugging (so it's not still redirected to self._fakeout). + # Note that the interactive output will go to *our* + # save_stdout, even if that's not the real sys.stdout; this + # allows us to write test cases for the set_trace behavior. + save_set_trace = pdb.set_trace + self.debugger = _OutputRedirectingPdb(save_stdout) + self.debugger.reset() + pdb.set_trace = self.debugger.set_trace + + # Patch linecache.getlines, so we can see the example's source + # when we're inside the debugger. + self.save_linecache_getlines = linecache.getlines + linecache.getlines = self.__patched_linecache_getlines + + try: + return self.__run(test, compileflags, out) + finally: + sys.stdout = save_stdout + pdb.set_trace = save_set_trace + linecache.getlines = self.save_linecache_getlines + if clear_globs: + test.globs.clear() + + class DocFileCase(doctest.DocFileCase): """Overrides to provide filename """ @@ -514,7 +740,8 @@ class ExtensionDoctest(doctests.Doctest): self.extension = tolist(options.doctestExtension) self.finder = DocTestFinder() self.parser = doctest.DocTestParser() - + self.globs = None + self.extraglobs = None def loadTestsFromExtensionModule(self,filename): bpath,mod = os.path.split(filename) @@ -529,14 +756,21 @@ class ExtensionDoctest(doctests.Doctest): # NOTE: the method below is almost a copy of the original one in nose, with # a few modifications to control output checking. - + def loadTestsFromModule(self, module): #print 'lTM',module # dbg if not self.matches(module.__name__): log.debug("Doctest doesn't want module %s", module) return - tests = self.finder.find(module) + + ## try: + ## print 'Globs:',self.globs.keys() # dbg + ## except: + ## pass + + tests = self.finder.find(module,globs=self.globs, + extraglobs=self.extraglobs) if not tests: return tests.sort() @@ -549,12 +783,13 @@ class ExtensionDoctest(doctests.Doctest): if not test.filename: test.filename = module_file - #yield DocTestCase(test) + # 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, + + yield DocTestCase(test, optionflags=optionflags, checker=checker) @@ -565,25 +800,21 @@ class ExtensionDoctest(doctests.Doctest): for t in self.loadTestsFromExtensionModule(filename): yield t else: - ## for t in list(doctests.Doctest.loadTestsFromFile(self,filename)): - ## yield t - pass - - if self.extension and anyp(filename.endswith, self.extension): - name = os.path.basename(filename) - dh = open(filename) - try: - doc = dh.read() - finally: - dh.close() - test = self.parser.get_doctest( - doc, globs={'__file__': filename}, name=name, - filename=filename, lineno=0) - if test.examples: - #print 'FileCase:',test.examples # dbg - yield DocFileCase(test) - else: - yield False # no tests to load + if self.extension and anyp(filename.endswith, self.extension): + name = os.path.basename(filename) + dh = open(filename) + try: + doc = dh.read() + finally: + dh.close() + test = self.parser.get_doctest( + doc, globs={'__file__': filename}, name=name, + filename=filename, lineno=0) + if test.examples: + #print 'FileCase:',test.examples # dbg + yield DocFileCase(test) + else: + yield False # no tests to load def wantFile(self,filename): """Return whether the given filename should be scanned for tests. @@ -591,7 +822,7 @@ class ExtensionDoctest(doctests.Doctest): Modified version that accepts extension modules as valid containers for doctests. """ - print 'Filename:',filename # dbg + #print 'Filename:',filename # dbg # temporarily hardcoded list, will move to driver later exclude = ['IPython/external/', @@ -626,3 +857,11 @@ class IPythonDoctest(ExtensionDoctest): self.extension = tolist(options.doctestExtension) self.parser = IPDocTestParser() self.finder = DocTestFinder(parser=self.parser) + + # XXX - we need to run in the ipython user's namespace, but doing so is + # breaking normal doctests! + + #self.globs = _ip.user_ns + self.globs = None + + self.extraglobs = None diff --git a/IPython/testing/plugin/simplevars.py b/IPython/testing/plugin/simplevars.py new file mode 100644 index 0000000..b9286c7 --- /dev/null +++ b/IPython/testing/plugin/simplevars.py @@ -0,0 +1,2 @@ +x = 1 +print 'x is:',x diff --git a/IPython/testing/plugin/test_refs.py b/IPython/testing/plugin/test_refs.py index 904fdc1..c83551b 100644 --- a/IPython/testing/plugin/test_refs.py +++ b/IPython/testing/plugin/test_refs.py @@ -1,4 +1,117 @@ -def test_refs(): +# Module imports +# Std lib +import inspect + +# Third party + +# Our own +import decorators as dec + +#----------------------------------------------------------------------------- +# Utilities + +# Note: copied from OInspect, kept here so the testing stuff doesn't create +# circular dependencies and is easier to reuse. +def getargspec(obj): + """Get the names and default values of a function's arguments. + + A tuple of four things is returned: (args, varargs, varkw, defaults). + 'args' is a list of the argument names (it may contain nested lists). + 'varargs' and 'varkw' are the names of the * and ** arguments or None. + 'defaults' is an n-tuple of the default values of the last n arguments. + + Modified version of inspect.getargspec from the Python Standard + Library.""" + + if inspect.isfunction(obj): + func_obj = obj + elif inspect.ismethod(obj): + func_obj = obj.im_func + else: + raise TypeError, 'arg is not a Python function' + args, varargs, varkw = inspect.getargs(func_obj.func_code) + return args, varargs, varkw, func_obj.func_defaults + +#----------------------------------------------------------------------------- +# Testing functions + +def test_trivial(): + """A trivial passing test.""" + pass + + +@dec.skip +def test_deliberately_broken(): + """A deliberately broken test - we want to skip this one.""" + 1/0 + + +# Verify that we can correctly skip the doctest for a function at will, but +# that the docstring itself is NOT destroyed by the decorator. +@dec.skip_doctest +def doctest_bad(x,y=1,**k): + """A function whose doctest we need to skip. + + >>> 1+1 + 3 + """ + z=2 + + +def test_skip_dt_decorator(): + """Doctest-skipping decorator should preserve the docstring. + """ + # Careful: 'check' must be a *verbatim* copy of the doctest_bad docstring! + check = """A function whose doctest we need to skip. + + >>> 1+1 + 3 + """ + # Fetch the docstring from doctest_bad after decoration. + val = doctest_bad.__doc__ + + assert check==val,"doctest_bad docstrings don't match" + + +def test_skip_dt_decorator2(): + """Doctest-skipping decorator should preserve function signature. + """ + # Hardcoded correct answer + dtargs = (['x', 'y'], None, 'k', (1,)) + # Introspect out the value + dtargsr = getargspec(doctest_bad) + assert dtargsr==dtargs, \ + "Incorrectly reconstructed args for doctest_bad: %s" % (dtargsr,) + + +def doctest_run(): + """Test running a trivial script. + + In [13]: run simplevars.py + x is: 1 + """ + +#@dec.skip_doctest +def doctest_runvars(): + """Test that variables defined in scripts get loaded correcly via %run. + + In [13]: run simplevars.py + x is: 1 + + In [14]: x + Out[14]: 1 + """ + +def doctest_ivars(): + """Test that variables defined interactively are picked up. + In [5]: zz=1 + + In [6]: zz + Out[6]: 1 + """ + +@dec.skip_doctest +def doctest_refs(): """DocTest reference holding issues when running scripts. In [32]: run show_refs.py @@ -6,6 +119,4 @@ def test_refs(): In [33]: map(type,gc.get_referrers(c)) Out[33]: [] - """ - pass