inputhooks
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
"""Simple example using doctests.
|
||||
|
||||
This file just contains doctests both using plain python and yap_ipython prompts.
|
||||
All tests should be loaded by nose.
|
||||
"""
|
||||
|
||||
def pyfunc():
|
||||
"""Some pure python tests...
|
||||
|
||||
>>> pyfunc()
|
||||
'pyfunc'
|
||||
|
||||
>>> import os
|
||||
|
||||
>>> 2+3
|
||||
5
|
||||
|
||||
>>> for i in range(3):
|
||||
... print(i, end=' ')
|
||||
... print(i+1, end=' ')
|
||||
...
|
||||
0 1 1 2 2 3
|
||||
"""
|
||||
return 'pyfunc'
|
||||
|
||||
def ipfunc():
|
||||
"""Some ipython tests...
|
||||
|
||||
In [1]: import os
|
||||
|
||||
In [3]: 2+3
|
||||
Out[3]: 5
|
||||
|
||||
In [26]: for i in range(3):
|
||||
....: print(i, end=' ')
|
||||
....: print(i+1, end=' ')
|
||||
....:
|
||||
0 1 1 2 2 3
|
||||
|
||||
|
||||
Examples that access the operating system work:
|
||||
|
||||
In [1]: !echo hello
|
||||
hello
|
||||
|
||||
In [2]: !echo hello > /tmp/foo_iptest
|
||||
|
||||
In [3]: !cat /tmp/foo_iptest
|
||||
hello
|
||||
|
||||
In [4]: rm -f /tmp/foo_iptest
|
||||
|
||||
It's OK to use '_' for the last result, but do NOT try to use yap_ipython's
|
||||
numbered history of _NN outputs, since those won't exist under the
|
||||
doctest environment:
|
||||
|
||||
In [7]: 'hi'
|
||||
Out[7]: 'hi'
|
||||
|
||||
In [8]: print(repr(_))
|
||||
'hi'
|
||||
|
||||
In [7]: 3+4
|
||||
Out[7]: 7
|
||||
|
||||
In [8]: _+3
|
||||
Out[8]: 10
|
||||
|
||||
In [9]: ipfunc()
|
||||
Out[9]: 'ipfunc'
|
||||
"""
|
||||
return 'ipfunc'
|
||||
|
||||
|
||||
def ranfunc():
|
||||
"""A function with some random output.
|
||||
|
||||
Normal examples are verified as usual:
|
||||
>>> 1+3
|
||||
4
|
||||
|
||||
But if you put '# random' in the output, it is ignored:
|
||||
>>> 1+3
|
||||
junk goes here... # random
|
||||
|
||||
>>> 1+2
|
||||
again, anything goes #random
|
||||
if multiline, the random mark is only needed once.
|
||||
|
||||
>>> 1+2
|
||||
You can also put the random marker at the end:
|
||||
# random
|
||||
|
||||
>>> 1+2
|
||||
# random
|
||||
.. or at the beginning.
|
||||
|
||||
More correct input is properly verified:
|
||||
>>> ranfunc()
|
||||
'ranfunc'
|
||||
"""
|
||||
return 'ranfunc'
|
||||
|
||||
|
||||
def random_all():
|
||||
"""A function where we ignore the output of ALL examples.
|
||||
|
||||
Examples:
|
||||
|
||||
# all-random
|
||||
|
||||
This mark tells the testing machinery that all subsequent examples should
|
||||
be treated as random (ignoring their output). They are still executed,
|
||||
so if a they raise an error, it will be detected as such, but their
|
||||
output is completely ignored.
|
||||
|
||||
>>> 1+3
|
||||
junk goes here...
|
||||
|
||||
>>> 1+3
|
||||
klasdfj;
|
||||
|
||||
>>> 1+2
|
||||
again, anything goes
|
||||
blah...
|
||||
"""
|
||||
pass
|
||||
|
||||
def iprand():
|
||||
"""Some ipython tests with random output.
|
||||
|
||||
In [7]: 3+4
|
||||
Out[7]: 7
|
||||
|
||||
In [8]: print('hello')
|
||||
world # random
|
||||
|
||||
In [9]: iprand()
|
||||
Out[9]: 'iprand'
|
||||
"""
|
||||
return 'iprand'
|
||||
|
||||
def iprand_all():
|
||||
"""Some ipython tests with fully random output.
|
||||
|
||||
# all-random
|
||||
|
||||
In [7]: 1
|
||||
Out[7]: 99
|
||||
|
||||
In [8]: print('hello')
|
||||
world
|
||||
|
||||
In [9]: iprand_all()
|
||||
Out[9]: 'junk'
|
||||
"""
|
||||
return 'iprand_all'
|
@@ -0,0 +1,764 @@
|
||||
"""Nose Plugin that supports yap_ipython doctests.
|
||||
|
||||
Limitations:
|
||||
|
||||
- When generating examples for use as doctests, make sure that you have
|
||||
pretty-printing OFF. This can be done either by setting the
|
||||
``PlainTextFormatter.pprint`` option in your configuration file to False, or
|
||||
by interactively disabling it with %Pprint. This is required so that yap_ipython
|
||||
output matches that of normal Python, which is used by doctest for internal
|
||||
execution.
|
||||
|
||||
- Do not rely on specific prompt numbers for results (such as using
|
||||
'_34==True', for example). For yap_ipython tests run via an external process the
|
||||
prompt numbers may be different, and yap_ipython tests run as normal python code
|
||||
won't even have these special _NN variables set at all.
|
||||
"""
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Module imports
|
||||
|
||||
# From the standard library
|
||||
import builtins as builtin_mod
|
||||
import doctest
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from importlib import import_module
|
||||
from io import StringIO
|
||||
|
||||
from testpath import modified_env
|
||||
|
||||
from inspect import getmodule
|
||||
|
||||
# We are overriding the default doctest runner, so we need to import a few
|
||||
# things from doctest directly
|
||||
from doctest import (REPORTING_FLAGS, REPORT_ONLY_FIRST_FAILURE,
|
||||
_unittest_reportflags, DocTestRunner,
|
||||
_extract_future_flags, pdb, _OutputRedirectingPdb,
|
||||
_exception_traceback,
|
||||
linecache)
|
||||
|
||||
# Third-party modules
|
||||
|
||||
from nose.plugins import doctests, Plugin
|
||||
from nose.util import anyp, tolist
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Module globals and other constants
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Classes and functions
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
def is_extension_module(filename):
|
||||
"""Return whether the given filename is an extension module.
|
||||
|
||||
This simply checks that the extension is either .so or .pyd.
|
||||
"""
|
||||
return os.path.splitext(filename)[1].lower() in ('.so','.pyd')
|
||||
|
||||
|
||||
class DocTestSkip(object):
|
||||
"""Object wrapper for doctests to be skipped."""
|
||||
|
||||
ds_skip = """Doctest to skip.
|
||||
>>> 1 #doctest: +SKIP
|
||||
"""
|
||||
|
||||
def __init__(self,obj):
|
||||
self.obj = obj
|
||||
|
||||
def __getattribute__(self,key):
|
||||
if key == '__doc__':
|
||||
return DocTestSkip.ds_skip
|
||||
else:
|
||||
return getattr(object.__getattribute__(self,'obj'),key)
|
||||
|
||||
# Modified version of the one in the stdlib, that fixes a python bug (doctests
|
||||
# not found in extension modules, http://bugs.python.org/issue3158)
|
||||
class DocTestFinder(doctest.DocTestFinder):
|
||||
|
||||
def _from_module(self, module, object):
|
||||
"""
|
||||
Return true if the given object is defined in the given
|
||||
module.
|
||||
"""
|
||||
if module is None:
|
||||
return True
|
||||
elif inspect.isfunction(object):
|
||||
return module.__dict__ is object.__globals__
|
||||
elif inspect.isbuiltin(object):
|
||||
return module.__name__ == object.__module__
|
||||
elif inspect.isclass(object):
|
||||
return module.__name__ == object.__module__
|
||||
elif inspect.ismethod(object):
|
||||
# This one may be a bug in cython that fails to correctly set the
|
||||
# __module__ attribute of methods, but since the same error is easy
|
||||
# to make by extension code writers, having this safety in place
|
||||
# isn't such a bad idea
|
||||
return module.__name__ == object.__self__.__class__.__module__
|
||||
elif inspect.getmodule(object) is not None:
|
||||
return module is inspect.getmodule(object)
|
||||
elif hasattr(object, '__module__'):
|
||||
return module.__name__ == object.__module__
|
||||
elif isinstance(object, property):
|
||||
return True # [XX] no way not be sure.
|
||||
elif inspect.ismethoddescriptor(object):
|
||||
# Unbound PyQt signals reach this point in Python 3.4b3, and we want
|
||||
# to avoid throwing an error. See also http://bugs.python.org/issue3158
|
||||
return False
|
||||
else:
|
||||
raise ValueError("object must be a class or function, got %r" % object)
|
||||
|
||||
def _find(self, tests, obj, name, module, source_lines, globs, seen):
|
||||
"""
|
||||
Find tests for the given object and any contained objects, and
|
||||
add them to `tests`.
|
||||
"""
|
||||
print('_find for:', obj, name, module) # dbg
|
||||
if hasattr(obj,"skip_doctest"):
|
||||
#print 'SKIPPING DOCTEST FOR:',obj # dbg
|
||||
obj = DocTestSkip(obj)
|
||||
|
||||
doctest.DocTestFinder._find(self,tests, obj, name, module,
|
||||
source_lines, globs, seen)
|
||||
|
||||
# Below we re-run pieces of the above method with manual modifications,
|
||||
# because the original code is buggy and fails to correctly identify
|
||||
# doctests in extension modules.
|
||||
|
||||
# Local shorthands
|
||||
from inspect import isroutine, isclass
|
||||
|
||||
# Look for tests in a module's contained objects.
|
||||
if inspect.ismodule(obj) and self._recurse:
|
||||
for valname, val in obj.__dict__.items():
|
||||
valname1 = '%s.%s' % (name, valname)
|
||||
if ( (isroutine(val) or isclass(val))
|
||||
and self._from_module(module, val) ):
|
||||
|
||||
self._find(tests, val, valname1, module, source_lines,
|
||||
globs, seen)
|
||||
|
||||
# Look for tests in a class's contained objects.
|
||||
if inspect.isclass(obj) and self._recurse:
|
||||
#print 'RECURSE into class:',obj # dbg
|
||||
for valname, val in obj.__dict__.items():
|
||||
# Special handling for staticmethod/classmethod.
|
||||
if isinstance(val, staticmethod):
|
||||
val = getattr(obj, valname)
|
||||
if isinstance(val, classmethod):
|
||||
val = getattr(obj, valname).__func__
|
||||
|
||||
# Recurse to methods, properties, and nested classes.
|
||||
if ((inspect.isfunction(val) or inspect.isclass(val) or
|
||||
inspect.ismethod(val) or
|
||||
isinstance(val, property)) and
|
||||
self._from_module(module, val)):
|
||||
valname = '%s.%s' % (name, valname)
|
||||
self._find(tests, val, valname, module, source_lines,
|
||||
globs, seen)
|
||||
|
||||
|
||||
class IPDoctestOutputChecker(doctest.OutputChecker):
|
||||
"""Second-chance checker with support for random tests.
|
||||
|
||||
If the default comparison doesn't pass, this checker looks in the expected
|
||||
output string for flags that tell us to ignore the output.
|
||||
"""
|
||||
|
||||
random_re = re.compile(r'#\s*random\s+')
|
||||
|
||||
def check_output(self, want, got, optionflags):
|
||||
"""Check output, accepting special markers embedded in the output.
|
||||
|
||||
If the output didn't pass the default validation but the special string
|
||||
'#random' is included, we accept it."""
|
||||
|
||||
# Let the original tester verify first, in case people have valid tests
|
||||
# that happen to have a comment saying '#random' embedded in.
|
||||
ret = doctest.OutputChecker.check_output(self, want, got,
|
||||
optionflags)
|
||||
if not ret and self.random_re.search(want):
|
||||
#print >> sys.stderr, 'RANDOM OK:',want # dbg
|
||||
return True
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
class DocTestCase(doctests.DocTestCase):
|
||||
"""Proxy for DocTestCase: provides an address() method that
|
||||
returns the correct address for the doctest case. Otherwise
|
||||
acts as a proxy to the test case. To provide hints for address(),
|
||||
an obj may also be passed -- this will be used as the test object
|
||||
for purposes of determining the test address, if it is provided.
|
||||
"""
|
||||
|
||||
# Note: this method was taken from numpy's nosetester module.
|
||||
|
||||
# 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,
|
||||
optionflags=optionflags,
|
||||
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
|
||||
self._dt_test_globs_ori = test.globs
|
||||
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 the directory it was loaded from, so
|
||||
# things like %run work without too many contortions
|
||||
self._ori_dir = os.path.dirname(test.filename)
|
||||
|
||||
# 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
|
||||
|
||||
if not (optionflags & REPORTING_FLAGS):
|
||||
# The option flags don't include any reporting flags,
|
||||
# so add the default reporting flags
|
||||
optionflags |= _unittest_reportflags
|
||||
|
||||
try:
|
||||
# Save our current directory and switch out to the one where the
|
||||
# test was originally created, in case another doctest did a
|
||||
# directory change. We'll restore this in the finally clause.
|
||||
curdir = os.getcwd()
|
||||
#print 'runTest in dir:', self._ori_dir # dbg
|
||||
os.chdir(self._ori_dir)
|
||||
|
||||
runner.DIVIDER = "-"*70
|
||||
failures, tries = runner.run(test,out=new.write,
|
||||
clear_globs=False)
|
||||
finally:
|
||||
sys.stdout = old
|
||||
os.chdir(curdir)
|
||||
|
||||
if failures:
|
||||
raise self.failureException(self.format_failure(new.getvalue()))
|
||||
|
||||
def setUp(self):
|
||||
"""Modified test setup that syncs with ipython namespace"""
|
||||
#print "setUp test", self._dt_test.examples # dbg
|
||||
if isinstance(self._dt_test.examples[0], IPExample):
|
||||
# for yap_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).
|
||||
self.user_ns_orig = {}
|
||||
self.user_ns_orig.update(_ip.user_ns)
|
||||
_ip.user_ns.update(self._dt_test.globs)
|
||||
# We must remove the _ key in the namespace, so that Python's
|
||||
# doctest code sets it naturally
|
||||
_ip.user_ns.pop('_', None)
|
||||
_ip.user_ns['__builtins__'] = builtin_mod
|
||||
self._dt_test.globs = _ip.user_ns
|
||||
|
||||
super(DocTestCase, self).setUp()
|
||||
|
||||
def tearDown(self):
|
||||
|
||||
# Undo the test.globs reassignment we made, so that the parent class
|
||||
# teardown doesn't destroy the ipython namespace
|
||||
if isinstance(self._dt_test.examples[0], IPExample):
|
||||
self._dt_test.globs = self._dt_test_globs_ori
|
||||
_ip.user_ns.clear()
|
||||
_ip.user_ns.update(self.user_ns_orig)
|
||||
|
||||
# XXX - fperez: I am not sure if this is truly a bug in nose 0.11, but
|
||||
# it does look like one to me: its tearDown method tries to run
|
||||
#
|
||||
# delattr(builtin_mod, self._result_var)
|
||||
#
|
||||
# without checking that the attribute really is there; it implicitly
|
||||
# assumes it should have been set via displayhook. But if the
|
||||
# displayhook was never called, this doesn't necessarily happen. I
|
||||
# haven't been able to find a little self-contained example outside of
|
||||
# ipython that would show the problem so I can report it to the nose
|
||||
# team, but it does happen a lot in our code.
|
||||
#
|
||||
# So here, we just protect as narrowly as possible by trapping an
|
||||
# attribute error whose message would be the name of self._result_var,
|
||||
# and letting any other error propagate.
|
||||
try:
|
||||
super(DocTestCase, self).tearDown()
|
||||
except AttributeError as exc:
|
||||
if exc.args[0] != self._result_var:
|
||||
raise
|
||||
|
||||
|
||||
# A simple subclassing of the original with a different class name, so we can
|
||||
# distinguish and treat differently yap_ipython examples from pure python ones.
|
||||
class IPExample(doctest.Example): pass
|
||||
|
||||
|
||||
class IPExternalExample(doctest.Example):
|
||||
"""Doctest examples to be run in an external process."""
|
||||
|
||||
def __init__(self, source, want, exc_msg=None, lineno=0, indent=0,
|
||||
options=None):
|
||||
# Parent constructor
|
||||
doctest.Example.__init__(self,source,want,exc_msg,lineno,indent,options)
|
||||
|
||||
# An EXTRA newline is needed to prevent pexpect hangs
|
||||
self.source += '\n'
|
||||
|
||||
|
||||
class IPDocTestParser(doctest.DocTestParser):
|
||||
"""
|
||||
A class used to parse strings containing doctest examples.
|
||||
|
||||
Note: This is a version modified to properly recognize yap_ipython input and
|
||||
convert any yap_ipython examples into valid Python ones.
|
||||
"""
|
||||
# This regular expression is used to find doctest examples in a
|
||||
# string. It defines three groups: `source` is the source code
|
||||
# (including leading indentation and prompts); `indent` is the
|
||||
# indentation of the first (PS1) line of the source code; and
|
||||
# `want` is the expected output (including leading indentation).
|
||||
|
||||
# Classic Python prompts or default yap_ipython ones
|
||||
_PS1_PY = r'>>>'
|
||||
_PS2_PY = r'\.\.\.'
|
||||
|
||||
_PS1_IP = r'In\ \[\d+\]:'
|
||||
_PS2_IP = r'\ \ \ \.\.\.+:'
|
||||
|
||||
_RE_TPL = r'''
|
||||
# Source consists of a PS1 line followed by zero or more PS2 lines.
|
||||
(?P<source>
|
||||
(?:^(?P<indent> [ ]*) (?P<ps1> %s) .*) # PS1 line
|
||||
(?:\n [ ]* (?P<ps2> %s) .*)*) # PS2 lines
|
||||
\n? # a newline
|
||||
# Want consists of any non-blank lines that do not start with PS1.
|
||||
(?P<want> (?:(?![ ]*$) # Not a blank line
|
||||
(?![ ]*%s) # Not a line starting with PS1
|
||||
(?![ ]*%s) # Not a line starting with PS2
|
||||
.*$\n? # But any other line
|
||||
)*)
|
||||
'''
|
||||
|
||||
_EXAMPLE_RE_PY = re.compile( _RE_TPL % (_PS1_PY,_PS2_PY,_PS1_PY,_PS2_PY),
|
||||
re.MULTILINE | re.VERBOSE)
|
||||
|
||||
_EXAMPLE_RE_IP = re.compile( _RE_TPL % (_PS1_IP,_PS2_IP,_PS1_IP,_PS2_IP),
|
||||
re.MULTILINE | re.VERBOSE)
|
||||
|
||||
# Mark a test as being fully random. In this case, we simply append the
|
||||
# random marker ('#random') to each individual example's output. This way
|
||||
# we don't need to modify any other code.
|
||||
_RANDOM_TEST = re.compile(r'#\s*all-random\s+')
|
||||
|
||||
# Mark tests to be executed in an external process - currently unsupported.
|
||||
_EXTERNAL_IP = re.compile(r'#\s*ipdoctest:\s*EXTERNAL')
|
||||
|
||||
def ip2py(self,source):
|
||||
"""Convert input yap_ipython source into valid Python."""
|
||||
block = _ip.input_transformer_manager.transform_cell(source)
|
||||
if len(block.splitlines()) == 1:
|
||||
return _ip.prefilter(block)
|
||||
else:
|
||||
return block
|
||||
|
||||
def parse(self, string, name='<string>'):
|
||||
"""
|
||||
Divide the given string into examples and intervening text,
|
||||
and return them as a list of alternating Examples and strings.
|
||||
Line numbers for the Examples are 0-based. The optional
|
||||
argument `name` is a name identifying this string, and is only
|
||||
used for error messages.
|
||||
"""
|
||||
|
||||
#print 'Parse string:\n',string # dbg
|
||||
|
||||
string = string.expandtabs()
|
||||
# If all lines begin with the same indentation, then strip it.
|
||||
min_indent = self._min_indent(string)
|
||||
if min_indent > 0:
|
||||
string = '\n'.join([l[min_indent:] for l in string.split('\n')])
|
||||
|
||||
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:
|
||||
random_marker = ''
|
||||
|
||||
# Whether to convert the input from ipython to python syntax
|
||||
ip2py = False
|
||||
# Find all doctest examples in the string. First, try them as Python
|
||||
# examples, then as yap_ipython ones
|
||||
terms = list(self._EXAMPLE_RE_PY.finditer(string))
|
||||
if terms:
|
||||
# Normal Python example
|
||||
#print '-'*70 # dbg
|
||||
#print 'PyExample, Source:\n',string # dbg
|
||||
#print '-'*70 # dbg
|
||||
Example = doctest.Example
|
||||
else:
|
||||
# It's an ipython example. Note that IPExamples are run
|
||||
# in-process, so their syntax must be turned into valid python.
|
||||
# IPExternalExamples are run out-of-process (via pexpect) so they
|
||||
# don't need any filtering (a real ipython will be executing them).
|
||||
terms = list(self._EXAMPLE_RE_IP.finditer(string))
|
||||
if self._EXTERNAL_IP.search(string):
|
||||
#print '-'*70 # dbg
|
||||
#print 'IPExternalExample, Source:\n',string # dbg
|
||||
#print '-'*70 # dbg
|
||||
Example = IPExternalExample
|
||||
else:
|
||||
#print '-'*70 # dbg
|
||||
#print 'IPExample, Source:\n',string # dbg
|
||||
#print '-'*70 # dbg
|
||||
Example = IPExample
|
||||
ip2py = True
|
||||
|
||||
for m in terms:
|
||||
# Add the pre-example text to `output`.
|
||||
output.append(string[charno:m.start()])
|
||||
# Update lineno (lines before this example)
|
||||
lineno += string.count('\n', charno, m.start())
|
||||
# Extract info from the regexp match.
|
||||
(source, options, want, exc_msg) = \
|
||||
self._parse_example(m, name, lineno,ip2py)
|
||||
|
||||
# Append the random-output marker (it defaults to empty in most
|
||||
# cases, it's only non-empty for 'all-random' tests):
|
||||
want += random_marker
|
||||
|
||||
if Example is IPExternalExample:
|
||||
options[doctest.NORMALIZE_WHITESPACE] = True
|
||||
want += '\n'
|
||||
|
||||
# Create an Example, and add it to the list.
|
||||
if not self._IS_BLANK_OR_COMMENT(source):
|
||||
output.append(Example(source, want, exc_msg,
|
||||
lineno=lineno,
|
||||
indent=min_indent+len(m.group('indent')),
|
||||
options=options))
|
||||
# Update lineno (lines inside this example)
|
||||
lineno += string.count('\n', m.start(), m.end())
|
||||
# Update charno.
|
||||
charno = m.end()
|
||||
# Add any remaining post-example text to `output`.
|
||||
output.append(string[charno:])
|
||||
return output
|
||||
|
||||
def _parse_example(self, m, name, lineno,ip2py=False):
|
||||
"""
|
||||
Given a regular expression match from `_EXAMPLE_RE` (`m`),
|
||||
return a pair `(source, want)`, where `source` is the matched
|
||||
example's source code (with prompts and indentation stripped);
|
||||
and `want` is the example's expected output (with indentation
|
||||
stripped).
|
||||
|
||||
`name` is the string's name, and `lineno` is the line number
|
||||
where the example starts; both are used for error messages.
|
||||
|
||||
Optional:
|
||||
`ip2py`: if true, filter the input via yap_ipython to convert the syntax
|
||||
into valid python.
|
||||
"""
|
||||
|
||||
# Get the example's indentation level.
|
||||
indent = len(m.group('indent'))
|
||||
|
||||
# Divide source into lines; check that they're properly
|
||||
# indented; and then strip their indentation & prompts.
|
||||
source_lines = m.group('source').split('\n')
|
||||
|
||||
# We're using variable-length input prompts
|
||||
ps1 = m.group('ps1')
|
||||
ps2 = m.group('ps2')
|
||||
ps1_len = len(ps1)
|
||||
|
||||
self._check_prompt_blank(source_lines, indent, name, lineno,ps1_len)
|
||||
if ps2:
|
||||
self._check_prefix(source_lines[1:], ' '*indent + ps2, name, lineno)
|
||||
|
||||
source = '\n'.join([sl[indent+ps1_len+1:] for sl in source_lines])
|
||||
|
||||
if ip2py:
|
||||
# Convert source input from yap_ipython into valid Python syntax
|
||||
source = self.ip2py(source)
|
||||
|
||||
# Divide want into lines; check that it's properly indented; and
|
||||
# then strip the indentation. Spaces before the last newline should
|
||||
# be preserved, so plain rstrip() isn't good enough.
|
||||
want = m.group('want')
|
||||
want_lines = want.split('\n')
|
||||
if len(want_lines) > 1 and re.match(r' *$', want_lines[-1]):
|
||||
del want_lines[-1] # forget final newline & spaces after it
|
||||
self._check_prefix(want_lines, ' '*indent, name,
|
||||
lineno + len(source_lines))
|
||||
|
||||
# Remove ipython output prompt that might be present in the first line
|
||||
want_lines[0] = re.sub(r'Out\[\d+\]: \s*?\n?','',want_lines[0])
|
||||
|
||||
want = '\n'.join([wl[indent:] for wl in want_lines])
|
||||
|
||||
# If `want` contains a traceback message, then extract it.
|
||||
m = self._EXCEPTION_RE.match(want)
|
||||
if m:
|
||||
exc_msg = m.group('msg')
|
||||
else:
|
||||
exc_msg = None
|
||||
|
||||
# Extract options from the source.
|
||||
options = self._find_options(source, name, lineno)
|
||||
|
||||
return source, options, want, exc_msg
|
||||
|
||||
def _check_prompt_blank(self, lines, indent, name, lineno, ps1_len):
|
||||
"""
|
||||
Given the lines of a source string (including prompts and
|
||||
leading indentation), check to make sure that every prompt is
|
||||
followed by a space character. If any line is not followed by
|
||||
a space character, then raise ValueError.
|
||||
|
||||
Note: yap_ipython-modified version which takes the input prompt length as a
|
||||
parameter, so that prompts of variable length can be dealt with.
|
||||
"""
|
||||
space_idx = indent+ps1_len
|
||||
min_len = space_idx+1
|
||||
for i, line in enumerate(lines):
|
||||
if len(line) >= min_len and line[space_idx] != ' ':
|
||||
raise ValueError('line %r of the docstring for %s '
|
||||
'lacks blank after %s: %r' %
|
||||
(lineno+i+1, name,
|
||||
line[indent:space_idx], line))
|
||||
|
||||
|
||||
SKIP = doctest.register_optionflag('SKIP')
|
||||
|
||||
|
||||
class IPDocTestRunner(doctest.DocTestRunner,object):
|
||||
"""Test runner that synchronizes the yap_ipython namespace with test globals.
|
||||
"""
|
||||
|
||||
def run(self, test, compileflags=None, out=None, clear_globs=True):
|
||||
|
||||
# Hack: ipython needs access to the execution context of the example,
|
||||
# so that it can propagate user variables loaded by %run into
|
||||
# test.globs. We put them here into our modified %run as a function
|
||||
# attribute. Our new %run will then only make the namespace update
|
||||
# when called (rather than unconconditionally updating test.globs here
|
||||
# for all examples, most of which won't be calling %run anyway).
|
||||
#_ip._ipdoctest_test_globs = test.globs
|
||||
#_ip._ipdoctest_test_filename = test.filename
|
||||
|
||||
test.globs.update(_ip.user_ns)
|
||||
|
||||
# Override terminal size to standardise traceback format
|
||||
with modified_env({'COLUMNS': '80', 'LINES': '24'}):
|
||||
return super(IPDocTestRunner,self).run(test,
|
||||
compileflags,out,clear_globs)
|
||||
|
||||
|
||||
class DocFileCase(doctest.DocFileCase):
|
||||
"""Overrides to provide filename
|
||||
"""
|
||||
def address(self):
|
||||
return (self._dt_test.filename, None, None)
|
||||
|
||||
|
||||
class ExtensionDoctest(doctests.Doctest):
|
||||
"""Nose Plugin that supports doctests in extension modules.
|
||||
"""
|
||||
name = 'extdoctest' # call nosetests with --with-extdoctest
|
||||
enabled = True
|
||||
|
||||
def options(self, parser, env=os.environ):
|
||||
Plugin.options(self, parser, env)
|
||||
parser.add_option('--doctest-tests', action='store_true',
|
||||
dest='doctest_tests',
|
||||
default=env.get('NOSE_DOCTEST_TESTS',True),
|
||||
help="Also look for doctests in test modules. "
|
||||
"Note that classes, methods and functions should "
|
||||
"have either doctests or non-doctest tests, "
|
||||
"not both. [NOSE_DOCTEST_TESTS]")
|
||||
parser.add_option('--doctest-extension', action="append",
|
||||
dest="doctestExtension",
|
||||
help="Also look for doctests in files with "
|
||||
"this extension [NOSE_DOCTEST_EXTENSION]")
|
||||
# Set the default as a list, if given in env; otherwise
|
||||
# an additional value set on the command line will cause
|
||||
# an error.
|
||||
env_setting = env.get('NOSE_DOCTEST_EXTENSION')
|
||||
if env_setting is not None:
|
||||
parser.set_defaults(doctestExtension=tolist(env_setting))
|
||||
|
||||
|
||||
def configure(self, options, config):
|
||||
Plugin.configure(self, options, config)
|
||||
# Pull standard doctest plugin out of config; we will do doctesting
|
||||
config.plugins.plugins = [p for p in config.plugins.plugins
|
||||
if p.name != 'doctest']
|
||||
self.doctest_tests = options.doctest_tests
|
||||
self.extension = tolist(options.doctestExtension)
|
||||
|
||||
self.parser = doctest.DocTestParser()
|
||||
self.finder = DocTestFinder()
|
||||
self.checker = IPDoctestOutputChecker()
|
||||
self.globs = None
|
||||
self.extraglobs = None
|
||||
|
||||
|
||||
def loadTestsFromExtensionModule(self,filename):
|
||||
bpath,mod = os.path.split(filename)
|
||||
modname = os.path.splitext(mod)[0]
|
||||
try:
|
||||
sys.path.append(bpath)
|
||||
module = import_module(modname)
|
||||
tests = list(self.loadTestsFromModule(module))
|
||||
finally:
|
||||
sys.path.pop()
|
||||
return tests
|
||||
|
||||
# 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 '*** ipdoctest - lTM',module # dbg
|
||||
|
||||
if not self.matches(module.__name__):
|
||||
log.debug("Doctest doesn't want module %s", module)
|
||||
return
|
||||
|
||||
tests = self.finder.find(module,globs=self.globs,
|
||||
extraglobs=self.extraglobs)
|
||||
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'):
|
||||
module_file = module_file[:-1]
|
||||
for test in tests:
|
||||
if not test.examples:
|
||||
continue
|
||||
if not test.filename:
|
||||
test.filename = module_file
|
||||
|
||||
yield DocTestCase(test,
|
||||
optionflags=optionflags,
|
||||
checker=self.checker)
|
||||
|
||||
|
||||
def loadTestsFromFile(self, filename):
|
||||
#print "ipdoctest - from file", filename # dbg
|
||||
if is_extension_module(filename):
|
||||
for t in self.loadTestsFromExtensionModule(filename):
|
||||
yield t
|
||||
else:
|
||||
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
|
||||
|
||||
|
||||
class IPythonDoctest(ExtensionDoctest):
|
||||
"""Nose Plugin that supports doctests in extension modules.
|
||||
"""
|
||||
name = 'ipdoctest' # call nosetests with --with-ipdoctest
|
||||
enabled = True
|
||||
|
||||
def makeTest(self, obj, parent):
|
||||
"""Look for doctests in the given object, which will be a
|
||||
function, method or class.
|
||||
"""
|
||||
#print 'Plugin analyzing:', obj, parent # dbg
|
||||
# always use whitespace and ellipsis options
|
||||
optionflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
|
||||
|
||||
doctests = self.finder.find(obj, module=getmodule(parent))
|
||||
if doctests:
|
||||
for test in doctests:
|
||||
if len(test.examples) == 0:
|
||||
continue
|
||||
|
||||
yield DocTestCase(test, obj=obj,
|
||||
optionflags=optionflags,
|
||||
checker=self.checker)
|
||||
|
||||
def options(self, parser, env=os.environ):
|
||||
#print "Options for nose plugin:", self.name # dbg
|
||||
Plugin.options(self, parser, env)
|
||||
parser.add_option('--ipdoctest-tests', action='store_true',
|
||||
dest='ipdoctest_tests',
|
||||
default=env.get('NOSE_IPDOCTEST_TESTS',True),
|
||||
help="Also look for doctests in test modules. "
|
||||
"Note that classes, methods and functions should "
|
||||
"have either doctests or non-doctest tests, "
|
||||
"not both. [NOSE_IPDOCTEST_TESTS]")
|
||||
parser.add_option('--ipdoctest-extension', action="append",
|
||||
dest="ipdoctest_extension",
|
||||
help="Also look for doctests in files with "
|
||||
"this extension [NOSE_IPDOCTEST_EXTENSION]")
|
||||
# Set the default as a list, if given in env; otherwise
|
||||
# an additional value set on the command line will cause
|
||||
# an error.
|
||||
env_setting = env.get('NOSE_IPDOCTEST_EXTENSION')
|
||||
if env_setting is not None:
|
||||
parser.set_defaults(ipdoctest_extension=tolist(env_setting))
|
||||
|
||||
def configure(self, options, config):
|
||||
#print "Configuring nose plugin:", self.name # dbg
|
||||
Plugin.configure(self, options, config)
|
||||
# Pull standard doctest plugin out of config; we will do doctesting
|
||||
config.plugins.plugins = [p for p in config.plugins.plugins
|
||||
if p.name != 'doctest']
|
||||
self.doctest_tests = options.ipdoctest_tests
|
||||
self.extension = tolist(options.ipdoctest_extension)
|
||||
|
||||
self.parser = IPDocTestParser()
|
||||
self.finder = DocTestFinder(parser=self.parser)
|
||||
self.checker = IPDoctestOutputChecker()
|
||||
self.globs = None
|
||||
self.extraglobs = None
|
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env python
|
||||
"""Nose-based test runner.
|
||||
"""
|
||||
|
||||
from nose.core import main
|
||||
from nose.plugins.builtin import plugins
|
||||
from nose.plugins.doctests import Doctest
|
||||
|
||||
from . import ipdoctest
|
||||
from .ipdoctest import IPDocTestRunner
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('WARNING: this code is incomplete!')
|
||||
print()
|
||||
|
||||
pp = [x() for x in plugins] # activate all builtin plugins first
|
||||
main(testRunner=IPDocTestRunner(),
|
||||
plugins=pp+[ipdoctest.IPythonDoctest(),Doctest()])
|
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env python
|
||||
"""A Nose plugin to support yap_ipython doctests.
|
||||
"""
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
setup(name='yap_ipython doctest plugin',
|
||||
version='0.1',
|
||||
author='The yap_ipython Team',
|
||||
description = 'Nose plugin to load yap_ipython-extended doctests',
|
||||
license = 'LGPL',
|
||||
py_modules = ['ipdoctest'],
|
||||
entry_points = {
|
||||
'nose.plugins.0.10': ['ipdoctest = ipdoctest:IPythonDoctest',
|
||||
'extdoctest = ipdoctest:ExtensionDoctest',
|
||||
],
|
||||
},
|
||||
)
|
@@ -0,0 +1,19 @@
|
||||
"""Simple script to show reference holding behavior.
|
||||
|
||||
This is used by a companion test case.
|
||||
"""
|
||||
|
||||
import gc
|
||||
|
||||
class C(object):
|
||||
def __del__(self):
|
||||
pass
|
||||
#print 'deleting object...' # dbg
|
||||
|
||||
if __name__ == '__main__':
|
||||
c = C()
|
||||
|
||||
c_refs = gc.get_referrers(c)
|
||||
ref_ids = list(map(id,c_refs))
|
||||
|
||||
print('c referrers:',list(map(type,c_refs)))
|
@@ -0,0 +1,33 @@
|
||||
"""Simple example using doctests.
|
||||
|
||||
This file just contains doctests both using plain python and yap_ipython prompts.
|
||||
All tests should be loaded by nose.
|
||||
"""
|
||||
|
||||
def pyfunc():
|
||||
"""Some pure python tests...
|
||||
|
||||
>>> pyfunc()
|
||||
'pyfunc'
|
||||
|
||||
>>> import os
|
||||
|
||||
>>> 2+3
|
||||
5
|
||||
|
||||
>>> for i in range(3):
|
||||
... print(i, end=' ')
|
||||
... print(i+1, end=' ')
|
||||
...
|
||||
0 1 1 2 2 3
|
||||
"""
|
||||
return 'pyfunc'
|
||||
|
||||
|
||||
def ipyfunc2():
|
||||
"""Some pure python tests...
|
||||
|
||||
>>> 1+1
|
||||
2
|
||||
"""
|
||||
return 'pyfunc2'
|
@@ -0,0 +1,2 @@
|
||||
x = 1
|
||||
print('x is:',x)
|
@@ -0,0 +1,80 @@
|
||||
"""Tests for the ipdoctest machinery itself.
|
||||
|
||||
Note: in a file named test_X, functions whose only test is their docstring (as
|
||||
a doctest) and which have no test functionality of their own, should be called
|
||||
'doctest_foo' instead of 'test_foo', otherwise they get double-counted (the
|
||||
empty function call is counted as a test, which just inflates tests numbers
|
||||
artificially).
|
||||
"""
|
||||
from yap_ipython.utils.py3compat import doctest_refactor_print
|
||||
|
||||
@doctest_refactor_print
|
||||
def doctest_simple():
|
||||
"""ipdoctest must handle simple inputs
|
||||
|
||||
In [1]: 1
|
||||
Out[1]: 1
|
||||
|
||||
In [2]: print 1
|
||||
1
|
||||
"""
|
||||
|
||||
@doctest_refactor_print
|
||||
def doctest_multiline1():
|
||||
"""The ipdoctest machinery must handle multiline examples gracefully.
|
||||
|
||||
In [2]: for i in range(4):
|
||||
...: print i
|
||||
...:
|
||||
0
|
||||
1
|
||||
2
|
||||
3
|
||||
"""
|
||||
|
||||
@doctest_refactor_print
|
||||
def doctest_multiline2():
|
||||
"""Multiline examples that define functions and print output.
|
||||
|
||||
In [7]: def f(x):
|
||||
...: return x+1
|
||||
...:
|
||||
|
||||
In [8]: f(1)
|
||||
Out[8]: 2
|
||||
|
||||
In [9]: def g(x):
|
||||
...: print 'x is:',x
|
||||
...:
|
||||
|
||||
In [10]: g(1)
|
||||
x is: 1
|
||||
|
||||
In [11]: g('hello')
|
||||
x is: hello
|
||||
"""
|
||||
|
||||
|
||||
def doctest_multiline3():
|
||||
"""Multiline examples with blank lines.
|
||||
|
||||
In [12]: def h(x):
|
||||
....: if x>1:
|
||||
....: return x**2
|
||||
....: # To leave a blank line in the input, you must mark it
|
||||
....: # with a comment character:
|
||||
....: #
|
||||
....: # otherwise the doctest parser gets confused.
|
||||
....: else:
|
||||
....: return -1
|
||||
....:
|
||||
|
||||
In [13]: h(5)
|
||||
Out[13]: 25
|
||||
|
||||
In [14]: h(1)
|
||||
Out[14]: -1
|
||||
|
||||
In [15]: h(0)
|
||||
Out[15]: -1
|
||||
"""
|
@@ -0,0 +1,46 @@
|
||||
"""Some simple tests for the plugin while running scripts.
|
||||
"""
|
||||
# Module imports
|
||||
# Std lib
|
||||
import inspect
|
||||
|
||||
# Our own
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Testing functions
|
||||
|
||||
def test_trivial():
|
||||
"""A trivial passing test."""
|
||||
pass
|
||||
|
||||
def doctest_run():
|
||||
"""Test running a trivial script.
|
||||
|
||||
In [13]: run simplevars.py
|
||||
x is: 1
|
||||
"""
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
def doctest_refs():
|
||||
"""DocTest reference holding issues when running scripts.
|
||||
|
||||
In [32]: run show_refs.py
|
||||
c referrers: [<... 'dict'>]
|
||||
"""
|
@@ -0,0 +1,10 @@
|
||||
# encoding: utf-8
|
||||
__docformat__ = "restructuredtext en"
|
||||
#-------------------------------------------------------------------------------
|
||||
# Copyright (C) 2005 Fernando Perez <fperez@colorado.edu>
|
||||
# Brian E Granger <ellisonbg@gmail.com>
|
||||
# Benjamin Ragan-Kelley <benjaminrk@gmail.com>
|
||||
#
|
||||
# Distributed under the terms of the BSD License. The full license is in
|
||||
# the file COPYING, distributed as part of this software.
|
||||
#-------------------------------------------------------------------------------
|
@@ -0,0 +1,164 @@
|
||||
"""Tests for the decorators we've created for yap_ipython.
|
||||
"""
|
||||
|
||||
# Module imports
|
||||
# Std lib
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
# Third party
|
||||
import nose.tools as nt
|
||||
|
||||
# Our own
|
||||
from yap_ipython.testing 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.__func__
|
||||
else:
|
||||
raise TypeError('arg is not a Python function')
|
||||
args, varargs, varkw = inspect.getargs(func_obj.__code__)
|
||||
return args, varargs, varkw, func_obj.__defaults__
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Testing functions
|
||||
|
||||
@dec.as_unittest
|
||||
def trivial():
|
||||
"""A trivial test"""
|
||||
pass
|
||||
|
||||
|
||||
@dec.skip
|
||||
def test_deliberately_broken():
|
||||
"""A deliberately broken test - we want to skip this one."""
|
||||
1/0
|
||||
|
||||
@dec.skip('Testing the skip decorator')
|
||||
def test_deliberately_broken2():
|
||||
"""Another 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.
|
||||
def doctest_bad(x,y=1,**k):
|
||||
"""A function whose doctest we need to skip.
|
||||
|
||||
>>> 1+1
|
||||
3
|
||||
"""
|
||||
print('x:',x)
|
||||
print('y:',y)
|
||||
print('k:',k)
|
||||
|
||||
|
||||
def call_doctest_bad():
|
||||
"""Check that we can still call the decorated functions.
|
||||
|
||||
>>> doctest_bad(3,y=4)
|
||||
x: 3
|
||||
y: 4
|
||||
k: {}
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
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__
|
||||
|
||||
nt.assert_equal(check,val,"doctest_bad docstrings don't match")
|
||||
|
||||
|
||||
# Doctest skipping should work for class methods too
|
||||
class FooClass(object):
|
||||
"""FooClass
|
||||
|
||||
Example:
|
||||
|
||||
>>> 1+1
|
||||
2
|
||||
"""
|
||||
|
||||
def __init__(self,x):
|
||||
"""Make a FooClass.
|
||||
|
||||
Example:
|
||||
|
||||
>>> f = FooClass(3)
|
||||
junk
|
||||
"""
|
||||
print('Making a FooClass.')
|
||||
self.x = x
|
||||
|
||||
def bar(self,y):
|
||||
"""Example:
|
||||
|
||||
>>> ff = FooClass(3)
|
||||
>>> ff.bar(0)
|
||||
boom!
|
||||
>>> 1/0
|
||||
bam!
|
||||
"""
|
||||
return 1/y
|
||||
|
||||
def baz(self,y):
|
||||
"""Example:
|
||||
|
||||
>>> ff2 = FooClass(3)
|
||||
Making a FooClass.
|
||||
>>> ff2.baz(3)
|
||||
True
|
||||
"""
|
||||
return self.x==y
|
||||
|
||||
|
||||
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,)
|
||||
|
||||
|
||||
@dec.skip_linux
|
||||
def test_linux():
|
||||
nt.assert_false(sys.platform.startswith('linux'),"This test can't run under linux")
|
||||
|
||||
@dec.skip_win32
|
||||
def test_win32():
|
||||
nt.assert_not_equal(sys.platform,'win32',"This test can't run under windows")
|
||||
|
||||
@dec.skip_osx
|
||||
def test_osx():
|
||||
nt.assert_not_equal(sys.platform,'darwin',"This test can't run under osx")
|
||||
|
@@ -0,0 +1,137 @@
|
||||
"""Tests for yap_ipython's test support utilities.
|
||||
|
||||
These are decorators that allow standalone functions and docstrings to be seen
|
||||
as tests by unittest, replicating some of nose's functionality. Additionally,
|
||||
yap_ipython-syntax docstrings can be auto-converted to '>>>' so that ipython
|
||||
sessions can be copy-pasted as tests.
|
||||
|
||||
This file can be run as a script, and it will call unittest.main(). We must
|
||||
check that it works with unittest as well as with nose...
|
||||
|
||||
|
||||
Notes:
|
||||
|
||||
- Using nosetests --with-doctest --doctest-tests testfile.py
|
||||
will find docstrings as tests wherever they are, even in methods. But
|
||||
if we use ipython syntax in the docstrings, they must be decorated with
|
||||
@ipdocstring. This is OK for test-only code, but not for user-facing
|
||||
docstrings where we want to keep the ipython syntax.
|
||||
|
||||
- Using nosetests --with-doctest file.py
|
||||
also finds doctests if the file name doesn't have 'test' in it, because it is
|
||||
treated like a normal module. But if nose treats the file like a test file,
|
||||
then for normal classes to be doctested the extra --doctest-tests is
|
||||
necessary.
|
||||
|
||||
- running this script with python (it has a __main__ section at the end) misses
|
||||
one docstring test, the one embedded in the Foo object method. Since our
|
||||
approach relies on using decorators that create standalone TestCase
|
||||
instances, it can only be used for functions, not for methods of objects.
|
||||
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
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
from yap_ipython.testing.ipunittest import ipdoctest, ipdocstring
|
||||
from yap_ipython.utils.py3compat import doctest_refactor_print
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Test classes and functions
|
||||
#-----------------------------------------------------------------------------
|
||||
@ipdoctest
|
||||
@doctest_refactor_print
|
||||
def simple_dt():
|
||||
"""
|
||||
>>> print 1+1
|
||||
2
|
||||
"""
|
||||
|
||||
|
||||
@ipdoctest
|
||||
@doctest_refactor_print
|
||||
def ipdt_flush():
|
||||
"""
|
||||
In [20]: print 1
|
||||
1
|
||||
|
||||
In [26]: for i in range(4):
|
||||
....: print i
|
||||
....:
|
||||
....:
|
||||
0
|
||||
1
|
||||
2
|
||||
3
|
||||
|
||||
In [27]: 3+4
|
||||
Out[27]: 7
|
||||
"""
|
||||
|
||||
|
||||
@ipdoctest
|
||||
@doctest_refactor_print
|
||||
def ipdt_indented_test():
|
||||
"""
|
||||
In [20]: print 1
|
||||
1
|
||||
|
||||
In [26]: for i in range(4):
|
||||
....: print i
|
||||
....:
|
||||
....:
|
||||
0
|
||||
1
|
||||
2
|
||||
3
|
||||
|
||||
In [27]: 3+4
|
||||
Out[27]: 7
|
||||
"""
|
||||
|
||||
|
||||
class Foo(object):
|
||||
"""For methods, the normal decorator doesn't work.
|
||||
|
||||
But rewriting the docstring with ip2py does, *but only if using nose
|
||||
--with-doctest*. Do we want to have that as a dependency?
|
||||
"""
|
||||
|
||||
@ipdocstring
|
||||
@doctest_refactor_print
|
||||
def ipdt_method(self):
|
||||
"""
|
||||
In [20]: print 1
|
||||
1
|
||||
|
||||
In [26]: for i in range(4):
|
||||
....: print i
|
||||
....:
|
||||
....:
|
||||
0
|
||||
1
|
||||
2
|
||||
3
|
||||
|
||||
In [27]: 3+4
|
||||
Out[27]: 7
|
||||
"""
|
||||
|
||||
@doctest_refactor_print
|
||||
def normaldt_method(self):
|
||||
"""
|
||||
>>> print 1+1
|
||||
2
|
||||
"""
|
@@ -0,0 +1,136 @@
|
||||
# encoding: utf-8
|
||||
"""
|
||||
Tests for testing.tools
|
||||
"""
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Copyright (C) 2008-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
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import nose.tools as nt
|
||||
|
||||
from yap_ipython.testing import decorators as dec
|
||||
from yap_ipython.testing import tools as tt
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Tests
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
@dec.skip_win32
|
||||
def test_full_path_posix():
|
||||
spath = '/foo/bar.py'
|
||||
result = tt.full_path(spath,['a.txt','b.txt'])
|
||||
nt.assert_equal(result, ['/foo/a.txt', '/foo/b.txt'])
|
||||
spath = '/foo'
|
||||
result = tt.full_path(spath,['a.txt','b.txt'])
|
||||
nt.assert_equal(result, ['/a.txt', '/b.txt'])
|
||||
result = tt.full_path(spath,'a.txt')
|
||||
nt.assert_equal(result, ['/a.txt'])
|
||||
|
||||
|
||||
@dec.skip_if_not_win32
|
||||
def test_full_path_win32():
|
||||
spath = 'c:\\foo\\bar.py'
|
||||
result = tt.full_path(spath,['a.txt','b.txt'])
|
||||
nt.assert_equal(result, ['c:\\foo\\a.txt', 'c:\\foo\\b.txt'])
|
||||
spath = 'c:\\foo'
|
||||
result = tt.full_path(spath,['a.txt','b.txt'])
|
||||
nt.assert_equal(result, ['c:\\a.txt', 'c:\\b.txt'])
|
||||
result = tt.full_path(spath,'a.txt')
|
||||
nt.assert_equal(result, ['c:\\a.txt'])
|
||||
|
||||
|
||||
def test_parser():
|
||||
err = ("FAILED (errors=1)", 1, 0)
|
||||
fail = ("FAILED (failures=1)", 0, 1)
|
||||
both = ("FAILED (errors=1, failures=1)", 1, 1)
|
||||
for txt, nerr, nfail in [err, fail, both]:
|
||||
nerr1, nfail1 = tt.parse_test_output(txt)
|
||||
nt.assert_equal(nerr, nerr1)
|
||||
nt.assert_equal(nfail, nfail1)
|
||||
|
||||
|
||||
def test_temp_pyfile():
|
||||
src = 'pass\n'
|
||||
fname, fh = tt.temp_pyfile(src)
|
||||
assert os.path.isfile(fname)
|
||||
fh.close()
|
||||
with open(fname) as fh2:
|
||||
src2 = fh2.read()
|
||||
nt.assert_equal(src2, src)
|
||||
|
||||
class TestAssertPrints(unittest.TestCase):
|
||||
def test_passing(self):
|
||||
with tt.AssertPrints("abc"):
|
||||
print("abcd")
|
||||
print("def")
|
||||
print(b"ghi")
|
||||
|
||||
def test_failing(self):
|
||||
def func():
|
||||
with tt.AssertPrints("abc"):
|
||||
print("acd")
|
||||
print("def")
|
||||
print(b"ghi")
|
||||
|
||||
self.assertRaises(AssertionError, func)
|
||||
|
||||
|
||||
class Test_ipexec_validate(unittest.TestCase, tt.TempFileMixin):
|
||||
def test_main_path(self):
|
||||
"""Test with only stdout results.
|
||||
"""
|
||||
self.mktmp("print('A')\n"
|
||||
"print('B')\n"
|
||||
)
|
||||
out = "A\nB"
|
||||
tt.ipexec_validate(self.fname, out)
|
||||
|
||||
def test_main_path2(self):
|
||||
"""Test with only stdout results, expecting windows line endings.
|
||||
"""
|
||||
self.mktmp("print('A')\n"
|
||||
"print('B')\n"
|
||||
)
|
||||
out = "A\r\nB"
|
||||
tt.ipexec_validate(self.fname, out)
|
||||
|
||||
def test_exception_path(self):
|
||||
"""Test exception path in exception_validate.
|
||||
"""
|
||||
self.mktmp("import sys\n"
|
||||
"print('A')\n"
|
||||
"print('B')\n"
|
||||
"print('C', file=sys.stderr)\n"
|
||||
"print('D', file=sys.stderr)\n"
|
||||
)
|
||||
out = "A\nB"
|
||||
tt.ipexec_validate(self.fname, expected_out=out, expected_err="C\nD")
|
||||
|
||||
def test_exception_path2(self):
|
||||
"""Test exception path in exception_validate, expecting windows line endings.
|
||||
"""
|
||||
self.mktmp("import sys\n"
|
||||
"print('A')\n"
|
||||
"print('B')\n"
|
||||
"print('C', file=sys.stderr)\n"
|
||||
"print('D', file=sys.stderr)\n"
|
||||
)
|
||||
out = "A\r\nB"
|
||||
tt.ipexec_validate(self.fname, expected_out=out, expected_err="C\r\nD")
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
# tear down correctly the mixin,
|
||||
# unittest.TestCase.tearDown does nothing
|
||||
tt.TempFileMixin.tearDown(self)
|
Reference in New Issue
Block a user