177 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			177 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
|   | """Experimental code for cleaner support of yap_ipython syntax with unittest.
 | ||
|  | 
 | ||
|  | In yap_ipython up until 0.10, we've used very hacked up nose machinery for running | ||
|  | tests with yap_ipython special syntax, and this has proved to be extremely slow. | ||
|  | This module provides decorators to try a different approach, stemming from a | ||
|  | conversation Brian and I (FP) had about this problem Sept/09. | ||
|  | 
 | ||
|  | The goal is to be able to easily write simple functions that can be seen by | ||
|  | unittest as tests, and ultimately for these to support doctests with full | ||
|  | yap_ipython syntax.  Nose already offers this based on naming conventions and our | ||
|  | hackish plugins, but we are seeking to move away from nose dependencies if | ||
|  | possible. | ||
|  | 
 | ||
|  | This module follows a different approach, based on decorators. | ||
|  | 
 | ||
|  | - A decorator called @ipdoctest can mark any function as having a docstring | ||
|  |   that should be viewed as a doctest, but after syntax conversion. | ||
|  | 
 | ||
|  | Authors | ||
|  | ------- | ||
|  | 
 | ||
|  | - Fernando Perez <Fernando.Perez@berkeley.edu> | ||
|  | """
 | ||
|  | 
 | ||
|  | 
 | ||
|  | #----------------------------------------------------------------------------- | ||
|  | #  Copyright (C) 2009-2011  The yap_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. | ||
|  | #----------------------------------------------------------------------------- | ||
|  | 
 | ||
|  | #----------------------------------------------------------------------------- | ||
|  | # Imports | ||
|  | #----------------------------------------------------------------------------- | ||
|  | 
 | ||
|  | # Stdlib | ||
|  | import re | ||
|  | import unittest | ||
|  | from doctest import DocTestFinder, DocTestRunner, TestResults | ||
|  | 
 | ||
|  | #----------------------------------------------------------------------------- | ||
|  | # Classes and functions | ||
|  | #----------------------------------------------------------------------------- | ||
|  | 
 | ||
|  | def count_failures(runner): | ||
|  |     """Count number of failures in a doctest runner.
 | ||
|  | 
 | ||
|  |     Code modeled after the summarize() method in doctest. | ||
|  |     """
 | ||
|  |     return [TestResults(f, t) for f, t in runner._name2ft.values() if f > 0 ] | ||
|  | 
 | ||
|  | 
 | ||
|  | class IPython2PythonConverter(object): | ||
|  |     """Convert yap_ipython 'syntax' to valid Python.
 | ||
|  | 
 | ||
|  |     Eventually this code may grow to be the full yap_ipython syntax conversion | ||
|  |     implementation, but for now it only does prompt conversion."""
 | ||
|  |      | ||
|  |     def __init__(self): | ||
|  |         self.rps1 = re.compile(r'In\ \[\d+\]: ') | ||
|  |         self.rps2 = re.compile(r'\ \ \ \.\.\.+: ') | ||
|  |         self.rout = re.compile(r'Out\[\d+\]: \s*?\n?') | ||
|  |         self.pyps1 = '>>> ' | ||
|  |         self.pyps2 = '... ' | ||
|  |         self.rpyps1 = re.compile ('(\s*%s)(.*)$' % self.pyps1) | ||
|  |         self.rpyps2 = re.compile ('(\s*%s)(.*)$' % self.pyps2) | ||
|  | 
 | ||
|  |     def __call__(self, ds): | ||
|  |         """Convert yap_ipython prompts to python ones in a string.""" | ||
|  |         from . import globalipapp | ||
|  | 
 | ||
|  |         pyps1 = '>>> ' | ||
|  |         pyps2 = '... ' | ||
|  |         pyout = '' | ||
|  | 
 | ||
|  |         dnew = ds | ||
|  |         dnew = self.rps1.sub(pyps1, dnew) | ||
|  |         dnew = self.rps2.sub(pyps2, dnew) | ||
|  |         dnew = self.rout.sub(pyout, dnew) | ||
|  |         ip = globalipapp.get_ipython() | ||
|  | 
 | ||
|  |         # Convert input yap_ipython source into valid Python. | ||
|  |         out = [] | ||
|  |         newline = out.append | ||
|  |         for line in dnew.splitlines(): | ||
|  | 
 | ||
|  |             mps1 = self.rpyps1.match(line) | ||
|  |             if mps1 is not None: | ||
|  |                 prompt, text = mps1.groups() | ||
|  |                 newline(prompt+ip.prefilter(text, False)) | ||
|  |                 continue | ||
|  | 
 | ||
|  |             mps2 = self.rpyps2.match(line) | ||
|  |             if mps2 is not None: | ||
|  |                 prompt, text = mps2.groups() | ||
|  |                 newline(prompt+ip.prefilter(text, True)) | ||
|  |                 continue | ||
|  |              | ||
|  |             newline(line) | ||
|  |         newline('')  # ensure a closing newline, needed by doctest | ||
|  |         #print "PYSRC:", '\n'.join(out)  # dbg | ||
|  |         return '\n'.join(out) | ||
|  | 
 | ||
|  |     #return dnew | ||
|  | 
 | ||
|  | 
 | ||
|  | class Doc2UnitTester(object): | ||
|  |     """Class whose instances act as a decorator for docstring testing.
 | ||
|  | 
 | ||
|  |     In practice we're only likely to need one instance ever, made below (though | ||
|  |     no attempt is made at turning it into a singleton, there is no need for | ||
|  |     that). | ||
|  |     """
 | ||
|  |     def __init__(self, verbose=False): | ||
|  |         """New decorator.
 | ||
|  | 
 | ||
|  |         Parameters | ||
|  |         ---------- | ||
|  | 
 | ||
|  |         verbose : boolean, optional (False) | ||
|  |           Passed to the doctest finder and runner to control verbosity. | ||
|  |         """
 | ||
|  |         self.verbose = verbose | ||
|  |         # We can reuse the same finder for all instances | ||
|  |         self.finder = DocTestFinder(verbose=verbose, recurse=False) | ||
|  | 
 | ||
|  |     def __call__(self, func): | ||
|  |         """Use as a decorator: doctest a function's docstring as a unittest.
 | ||
|  |          | ||
|  |         This version runs normal doctests, but the idea is to make it later run | ||
|  |         ipython syntax instead."""
 | ||
|  | 
 | ||
|  |         # Capture the enclosing instance with a different name, so the new | ||
|  |         # class below can see it without confusion regarding its own 'self' | ||
|  |         # that will point to the test instance at runtime | ||
|  |         d2u = self | ||
|  | 
 | ||
|  |         # Rewrite the function's docstring to have python syntax | ||
|  |         if func.__doc__ is not None: | ||
|  |             func.__doc__ = ip2py(func.__doc__) | ||
|  | 
 | ||
|  |         # Now, create a tester object that is a real unittest instance, so | ||
|  |         # normal unittest machinery (or Nose, or Trial) can find it. | ||
|  |         class Tester(unittest.TestCase): | ||
|  |             def test(self): | ||
|  |                 # Make a new runner per function to be tested | ||
|  |                 runner = DocTestRunner(verbose=d2u.verbose) | ||
|  |                 map(runner.run, d2u.finder.find(func, func.__name__)) | ||
|  |                 failed = count_failures(runner) | ||
|  |                 if failed: | ||
|  |                     # Since we only looked at a single function's docstring, | ||
|  |                     # failed should contain at most one item.  More than that | ||
|  |                     # is a case we can't handle and should error out on | ||
|  |                     if len(failed) > 1: | ||
|  |                         err = "Invalid number of test results:" % failed | ||
|  |                         raise ValueError(err) | ||
|  |                     # Report a normal failure. | ||
|  |                     self.fail('failed doctests: %s' % str(failed[0])) | ||
|  |                      | ||
|  |         # Rename it so test reports have the original signature. | ||
|  |         Tester.__name__ = func.__name__ | ||
|  |         return Tester | ||
|  | 
 | ||
|  | 
 | ||
|  | def ipdocstring(func): | ||
|  |     """Change the function docstring via ip2py.
 | ||
|  |     """
 | ||
|  |     if func.__doc__ is not None: | ||
|  |         func.__doc__ = ip2py(func.__doc__) | ||
|  |     return func | ||
|  | 
 | ||
|  |          | ||
|  | # Make an instance of the classes for public use | ||
|  | ipdoctest = Doc2UnitTester() | ||
|  | ip2py = IPython2PythonConverter() |