|
"""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]> |
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import re |
|
import sys |
|
import unittest |
|
from doctest import DocTestFinder, DocTestRunner, TestResults |
|
from IPython.terminal.interactiveshell import InteractiveShell |
|
|
|
|
|
|
|
|
|
|
|
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(object): |
|
"""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() |
|
|
|
|
|
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('') |
|
|
|
return '\n'.join(out) |
|
|
|
|
|
|
|
|
|
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 |
|
|
|
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.""" |
|
|
|
|
|
|
|
|
|
d2u = self |
|
|
|
|
|
if func.__doc__ is not None: |
|
func.__doc__ = ip2py(func.__doc__) |
|
|
|
|
|
|
|
class Tester(unittest.TestCase): |
|
def test(self): |
|
|
|
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: |
|
|
|
|
|
|
|
if len(failed) > 1: |
|
err = "Invalid number of test results: %s" % failed |
|
raise ValueError(err) |
|
|
|
self.fail('failed doctests: %s' % str(failed[0])) |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
ipdoctest = Doc2UnitTester() |
|
ip2py = IPython2PythonConverter() |
|
|