Spaces:
Paused
Paused
| """Experimental code for cleaner support of IPython syntax with unittest. | |
| In IPython up until 0.10, we've used very hacked up nose machinery for running | |
| tests with 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 | |
| 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 <[email protected]> | |
| """ | |
| #----------------------------------------------------------------------------- | |
| # Copyright (C) 2009-2011 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. | |
| #----------------------------------------------------------------------------- | |
| #----------------------------------------------------------------------------- | |
| # Imports | |
| #----------------------------------------------------------------------------- | |
| # Stdlib | |
| import re | |
| import sys | |
| import unittest | |
| import builtins | |
| from doctest import DocTestFinder, DocTestRunner, TestResults | |
| from IPython.terminal.interactiveshell import InteractiveShell | |
| #----------------------------------------------------------------------------- | |
| # Classes and functions | |
| #----------------------------------------------------------------------------- | |
| def count_failures(runner): | |
| """Count number of failures in a doctest runner. | |
| Code modeled after the summarize() method in doctest. | |
| """ | |
| if sys.version_info < (3, 13): | |
| return [TestResults(f, t) for f, t in runner._name2ft.values() if f > 0] | |
| else: | |
| return [ | |
| TestResults(failure, try_) | |
| for failure, try_, skip in runner._stats.values() | |
| if failure > 0 | |
| ] | |
| class IPython2PythonConverter: | |
| """Convert IPython 'syntax' to valid Python. | |
| Eventually this code may grow to be the full 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 (r'(\s*%s)(.*)$' % self.pyps1) | |
| self.rpyps2 = re.compile (r'(\s*%s)(.*)$' % self.pyps2) | |
| def __call__(self, ds): | |
| """Convert 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 = InteractiveShell.instance() | |
| # Convert input 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: | |
| """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) | |
| for the_test in d2u.finder.find(func, func.__name__): | |
| runner.run(the_test) | |
| 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: %s" % 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() | |