Uploaded Test files

This commit is contained in:
Batuhan Berk Başoğlu 2020-11-12 11:05:57 -05:00
parent f584ad9d97
commit 2e81cb7d99
16627 changed files with 2065359 additions and 102444 deletions

View file

@ -0,0 +1,49 @@
"""Testing support (tools to test IPython itself).
"""
#-----------------------------------------------------------------------------
# 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.
#-----------------------------------------------------------------------------
import os
#-----------------------------------------------------------------------------
# Functions
#-----------------------------------------------------------------------------
# User-level entry point for testing
def test(**kwargs):
"""Run the entire IPython test suite.
Any of the options for run_iptestall() may be passed as keyword arguments.
For example::
IPython.test(testgroups=['lib', 'config', 'utils'], fast=2)
will run those three sections of the test suite, using two processes.
"""
# Do the import internally, so that this function doesn't increase total
# import time
from .iptestcontroller import run_iptestall, default_options
options = default_options()
for name, val in kwargs.items():
setattr(options, name, val)
run_iptestall(options)
#-----------------------------------------------------------------------------
# Constants
#-----------------------------------------------------------------------------
# We scale all timeouts via this factor, slow machines can increase it
IPYTHON_TESTING_TIMEOUT_SCALE = float(os.getenv(
'IPYTHON_TESTING_TIMEOUT_SCALE', 1))
# So nose doesn't try to run this as a test itself and we end up with an
# infinite test loop
test.__test__ = False

View file

@ -0,0 +1,3 @@
if __name__ == '__main__':
from IPython.testing import iptestcontroller
iptestcontroller.main()

View file

@ -0,0 +1,383 @@
# -*- coding: utf-8 -*-
"""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.
This module provides a set of useful decorators meant to be ready to use in
your own tests. See the bottom of the file for the ready-made ones, and if you
find yourself writing a new one that may be of generic use, add it here.
Included decorators:
Lightweight testing that remains unittest-compatible.
- An @as_unittest decorator can be used to tag any normal parameter-less
function as a unittest TestCase. Then, both nose and normal unittest will
recognize it as such. This will make it easier to migrate away from Nose if
we ever need/want to while maintaining very lightweight tests.
NOTE: This file contains IPython-specific decorators. Using the machinery in
IPython.external.decorators, we import either numpy.testing.decorators if numpy is
available, OR use equivalent code in IPython.external._decorators, which
we've copied verbatim from numpy.
"""
# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.
import os
import shutil
import sys
import tempfile
import unittest
import warnings
from importlib import import_module
from decorator import decorator
# Expose the unittest-driven decorators
from .ipunittest import ipdoctest, ipdocstring
# Grab the numpy-specific decorators which we keep in a file that we
# occasionally update from upstream: decorators.py is a copy of
# numpy.testing.decorators, we expose all of it here.
from IPython.external.decorators import knownfailureif
#-----------------------------------------------------------------------------
# Classes and functions
#-----------------------------------------------------------------------------
# Simple example of the basic idea
def as_unittest(func):
"""Decorator to make a simple function into a normal test via unittest."""
class Tester(unittest.TestCase):
def test(self):
func()
Tester.__name__ = func.__name__
return Tester
# 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.
"""
warnings.warn("The function `apply_wrapper` is deprecated since IPython 4.0",
DeprecationWarning, stacklevel=2)
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.
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')
>>> 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.")
>>> 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
"""
warnings.warn("The function `make_label_dec` is deprecated since IPython 4.0",
DeprecationWarning, stacklevel=2)
if isinstance(label, str):
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
# Inspired by numpy's skipif, but uses the full apply_wrapper utility to
# preserve function metadata better and allows the skip condition to be a
# callable.
def skipif(skip_condition, msg=None):
''' Make function raise SkipTest exception if skip_condition is true
Parameters
----------
skip_condition : bool or callable
Flag to determine whether to skip test. If the condition is a
callable, it is used at runtime to dynamically make the decision. This
is useful for tests that may require costly imports, to delay the cost
until the test suite is actually executed.
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.
'''
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
# Allow for both boolean or callable skip conditions.
if callable(skip_condition):
skip_val = skip_condition
else:
skip_val = lambda : skip_condition
def get_msg(func,msg=None):
"""Skip message with information about function being skipped."""
if msg is None: out = 'Test skipped due to test condition.'
else: out = msg
return "Skipping test: %s. %s" % (func.__name__,out)
# We need to define *two* skippers because Python doesn't allow both
# return with value and yield inside the same function.
def skipper_func(*args, **kwargs):
"""Skipper for normal test functions."""
if skip_val():
raise nose.SkipTest(get_msg(f,msg))
else:
return f(*args, **kwargs)
def skipper_gen(*args, **kwargs):
"""Skipper for test generators."""
if skip_val():
raise nose.SkipTest(get_msg(f,msg))
else:
for x in f(*args, **kwargs):
yield x
# Choose the right skipper to use when building the actual generator.
if nose.util.isgenerator(f):
skipper = skipper_gen
else:
skipper = skipper_func
return nose.tools.make_decorator(f)(skipper)
return skip_decorator
# A version with the condition set to true, common case just to attach a message
# to a skip decorator
def skip(msg=None):
"""Decorator factory - mark a test function for skipping from test suite.
Parameters
----------
msg : string
Optional message to be added.
Returns
-------
decorator : function
Decorator, which, when applied to a function, causes SkipTest
to be raised, with the optional message added.
"""
if msg and not isinstance(msg, str):
raise ValueError('invalid object passed to `@skip` decorator, did you '
'meant `@skip()` with brackets ?')
return skipif(True, msg)
def onlyif(condition, msg):
"""The reverse from skipif, see skipif for details."""
if callable(condition):
skip_condition = lambda : not condition()
else:
skip_condition = lambda : not condition
return skipif(skip_condition, msg)
#-----------------------------------------------------------------------------
# Utility functions for decorators
def module_not_available(module):
"""Can module be imported? Returns true if module does NOT import.
This is used to make a decorator to skip tests that require module to be
available, but delay the 'import numpy' to test execution time.
"""
try:
mod = import_module(module)
mod_not_avail = False
except ImportError:
mod_not_avail = True
return mod_not_avail
def decorated_dummy(dec, name):
"""Return a dummy function decorated with dec, with the given name.
Examples
--------
import IPython.testing.decorators as dec
setup = dec.decorated_dummy(dec.skip_if_no_x11, __name__)
"""
warnings.warn("The function `decorated_dummy` is deprecated since IPython 4.0",
DeprecationWarning, stacklevel=2)
dummy = lambda: None
dummy.__name__ = name
return dec(dummy)
#-----------------------------------------------------------------------------
# Decorators for public use
# Decorators to skip certain tests on specific platforms.
skip_win32 = skipif(sys.platform == 'win32',
"This test does not run under Windows")
skip_linux = skipif(sys.platform.startswith('linux'),
"This test does not run under Linux")
skip_osx = skipif(sys.platform == 'darwin',"This test does not run under OS X")
# Decorators to skip tests if not on specific platforms.
skip_if_not_win32 = skipif(sys.platform != 'win32',
"This test only runs under Windows")
skip_if_not_linux = skipif(not sys.platform.startswith('linux'),
"This test only runs under Linux")
skip_if_not_osx = skipif(sys.platform != 'darwin',
"This test only runs under OSX")
_x11_skip_cond = (sys.platform not in ('darwin', 'win32') and
os.environ.get('DISPLAY', '') == '')
_x11_skip_msg = "Skipped under *nix when X11/XOrg not available"
skip_if_no_x11 = skipif(_x11_skip_cond, _x11_skip_msg)
# Decorators to skip certain tests on specific platform/python combinations
skip_win32_py38 = skipif(sys.version_info > (3,8) and os.name == 'nt')
# not a decorator itself, returns a dummy function to be used as setup
def skip_file_no_x11(name):
warnings.warn("The function `skip_file_no_x11` is deprecated since IPython 4.0",
DeprecationWarning, stacklevel=2)
return decorated_dummy(skip_if_no_x11, name) if _x11_skip_cond else None
# Other skip decorators
# generic skip without module
skip_without = lambda mod: skipif(module_not_available(mod), "This test requires %s" % mod)
skipif_not_numpy = skip_without('numpy')
skipif_not_matplotlib = skip_without('matplotlib')
skipif_not_sympy = skip_without('sympy')
skip_known_failure = knownfailureif(True,'This test is known to fail')
# A null 'decorator', useful to make more readable code that needs to pick
# between different decorators based on OS or other conditions
null_deco = lambda f: f
# Some tests only run where we can use unicode paths. Note that we can't just
# check os.path.supports_unicode_filenames, which is always False on Linux.
try:
f = tempfile.NamedTemporaryFile(prefix=u"tmp€")
except UnicodeEncodeError:
unicode_paths = False
else:
unicode_paths = True
f.close()
onlyif_unicode_paths = onlyif(unicode_paths, ("This test is only applicable "
"where we can use unicode in filenames."))
def onlyif_cmds_exist(*commands):
"""
Decorator to skip test when at least one of `commands` is not found.
"""
for cmd in commands:
if not shutil.which(cmd):
return skip("This test runs only if command '{0}' "
"is installed".format(cmd))
return null_deco
def onlyif_any_cmd_exists(*commands):
"""
Decorator to skip test unless at least one of `commands` is found.
"""
warnings.warn("The function `onlyif_any_cmd_exists` is deprecated since IPython 4.0",
DeprecationWarning, stacklevel=2)
for cmd in commands:
if shutil.which(cmd):
return null_deco
return skip("This test runs only if one of the commands {0} "
"is installed".format(commands))

View file

@ -0,0 +1,137 @@
"""Global IPython app to support test running.
We must start our own ipython object and heavily muck with it so that all the
modifications IPython makes to system behavior don't send the doctest machinery
into a fit. This code should be considered a gross hack, but it gets the job
done.
"""
# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.
import builtins as builtin_mod
import sys
import types
import warnings
from . import tools
from IPython.core import page
from IPython.utils import io
from IPython.terminal.interactiveshell import TerminalInteractiveShell
class StreamProxy(io.IOStream):
"""Proxy for sys.stdout/err. This will request the stream *at call time*
allowing for nose's Capture plugin's redirection of sys.stdout/err.
Parameters
----------
name : str
The name of the stream. This will be requested anew at every call
"""
def __init__(self, name):
warnings.warn("StreamProxy is deprecated and unused as of IPython 5", DeprecationWarning,
stacklevel=2,
)
self.name=name
@property
def stream(self):
return getattr(sys, self.name)
def flush(self):
self.stream.flush()
def get_ipython():
# This will get replaced by the real thing once we start IPython below
return start_ipython()
# A couple of methods to override those in the running IPython to interact
# better with doctest (doctest captures on raw stdout, so we need to direct
# various types of output there otherwise it will miss them).
def xsys(self, cmd):
"""Replace the default system call with a capturing one for doctest.
"""
# We use getoutput, but we need to strip it because pexpect captures
# the trailing newline differently from commands.getoutput
print(self.getoutput(cmd, split=False, depth=1).rstrip(), end='', file=sys.stdout)
sys.stdout.flush()
def _showtraceback(self, etype, evalue, stb):
"""Print the traceback purely on stdout for doctest to capture it.
"""
print(self.InteractiveTB.stb2text(stb), file=sys.stdout)
def start_ipython():
"""Start a global IPython shell, which we need for IPython-specific syntax.
"""
global get_ipython
# This function should only ever run once!
if hasattr(start_ipython, 'already_called'):
return
start_ipython.already_called = True
# Store certain global objects that IPython modifies
_displayhook = sys.displayhook
_excepthook = sys.excepthook
_main = sys.modules.get('__main__')
# Create custom argv and namespaces for our IPython to be test-friendly
config = tools.default_config()
config.TerminalInteractiveShell.simple_prompt = True
# Create and initialize our test-friendly IPython instance.
shell = TerminalInteractiveShell.instance(config=config,
)
# A few more tweaks needed for playing nicely with doctests...
# remove history file
shell.tempfiles.append(config.HistoryManager.hist_file)
# These traps are normally only active for interactive use, set them
# permanently since we'll be mocking interactive sessions.
shell.builtin_trap.activate()
# Modify the IPython system call with one that uses getoutput, so that we
# can capture subcommands and print them to Python's stdout, otherwise the
# doctest machinery would miss them.
shell.system = types.MethodType(xsys, shell)
shell._showtraceback = types.MethodType(_showtraceback, shell)
# IPython is ready, now clean up some global state...
# Deactivate the various python system hooks added by ipython for
# interactive convenience so we don't confuse the doctest system
sys.modules['__main__'] = _main
sys.displayhook = _displayhook
sys.excepthook = _excepthook
# So that ipython magics and aliases can be doctested (they work by making
# a call into a global _ip object). Also make the top-level get_ipython
# now return this without recursively calling here again.
_ip = shell
get_ipython = _ip.get_ipython
builtin_mod._ip = _ip
builtin_mod.ip = _ip
builtin_mod.get_ipython = get_ipython
# Override paging, so we don't require user interaction during the tests.
def nopage(strng, start=0, screen_lines=0, pager_cmd=None):
if isinstance(strng, dict):
strng = strng.get('text/plain', '')
print(strng)
page.orig_page = page.pager_page
page.pager_page = nopage
return _ip

View file

@ -0,0 +1,460 @@
# -*- coding: utf-8 -*-
"""IPython Test Suite Runner.
This module provides a main entry point to a user script to test IPython
itself from the command line. There are two ways of running this script:
1. With the syntax `iptest all`. This runs our entire test suite by
calling this script (with different arguments) recursively. This
causes modules and package to be tested in different processes, using nose
or trial where appropriate.
2. With the regular nose syntax, like `iptest IPython -- -vvs`. In this form
the script simply calls nose, but with special command line flags and
plugins loaded. Options after `--` are passed to nose.
"""
# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.
import glob
from io import BytesIO
import os
import os.path as path
import sys
from threading import Thread, Lock, Event
import warnings
import nose.plugins.builtin
from nose.plugins.xunit import Xunit
from nose import SkipTest
from nose.core import TestProgram
from nose.plugins import Plugin
from nose.util import safe_str
from IPython import version_info
from IPython.utils.py3compat import decode
from IPython.utils.importstring import import_item
from IPython.testing.plugin.ipdoctest import IPythonDoctest
from IPython.external.decorators import KnownFailure, knownfailureif
pjoin = path.join
# Enable printing all warnings raise by IPython's modules
warnings.filterwarnings('ignore', message='.*Matplotlib is building the font cache.*', category=UserWarning, module='.*')
warnings.filterwarnings('error', message='.*', category=ResourceWarning, module='.*')
warnings.filterwarnings('error', message=".*{'config': True}.*", category=DeprecationWarning, module='IPy.*')
warnings.filterwarnings('default', message='.*', category=Warning, module='IPy.*')
warnings.filterwarnings('error', message='.*apply_wrapper.*', category=DeprecationWarning, module='.*')
warnings.filterwarnings('error', message='.*make_label_dec', category=DeprecationWarning, module='.*')
warnings.filterwarnings('error', message='.*decorated_dummy.*', category=DeprecationWarning, module='.*')
warnings.filterwarnings('error', message='.*skip_file_no_x11.*', category=DeprecationWarning, module='.*')
warnings.filterwarnings('error', message='.*onlyif_any_cmd_exists.*', category=DeprecationWarning, module='.*')
warnings.filterwarnings('error', message='.*disable_gui.*', category=DeprecationWarning, module='.*')
warnings.filterwarnings('error', message='.*ExceptionColors global is deprecated.*', category=DeprecationWarning, module='.*')
# Jedi older versions
warnings.filterwarnings(
'error', message='.*elementwise != comparison failed and.*', category=FutureWarning, module='.*')
if version_info < (6,):
# nose.tools renames all things from `camelCase` to `snake_case` which raise an
# warning with the runner they also import from standard import library. (as of Dec 2015)
# Ignore, let's revisit that in a couple of years for IPython 6.
warnings.filterwarnings(
'ignore', message='.*Please use assertEqual instead', category=Warning, module='IPython.*')
if version_info < (8,):
warnings.filterwarnings('ignore', message='.*Completer.complete.*',
category=PendingDeprecationWarning, module='.*')
else:
warnings.warn(
'Completer.complete was pending deprecation and should be changed to Deprecated', FutureWarning)
# ------------------------------------------------------------------------------
# Monkeypatch Xunit to count known failures as skipped.
# ------------------------------------------------------------------------------
def monkeypatch_xunit():
try:
dec.knownfailureif(True)(lambda: None)()
except Exception as e:
KnownFailureTest = type(e)
def addError(self, test, err, capt=None):
if issubclass(err[0], KnownFailureTest):
err = (SkipTest,) + err[1:]
return self.orig_addError(test, err, capt)
Xunit.orig_addError = Xunit.addError
Xunit.addError = addError
#-----------------------------------------------------------------------------
# Check which dependencies are installed and greater than minimum version.
#-----------------------------------------------------------------------------
def extract_version(mod):
return mod.__version__
def test_for(item, min_version=None, callback=extract_version):
"""Test to see if item is importable, and optionally check against a minimum
version.
If min_version is given, the default behavior is to check against the
`__version__` attribute of the item, but specifying `callback` allows you to
extract the value you are interested in. e.g::
In [1]: import sys
In [2]: from IPython.testing.iptest import test_for
In [3]: test_for('sys', (2,6), callback=lambda sys: sys.version_info)
Out[3]: True
"""
try:
check = import_item(item)
except (ImportError, RuntimeError):
# GTK reports Runtime error if it can't be initialized even if it's
# importable.
return False
else:
if min_version:
if callback:
# extra processing step to get version to compare
check = callback(check)
return check >= min_version
else:
return True
# Global dict where we can store information on what we have and what we don't
# have available at test run time
have = {'matplotlib': test_for('matplotlib'),
'pygments': test_for('pygments'),
'sqlite3': test_for('sqlite3')}
#-----------------------------------------------------------------------------
# Test suite definitions
#-----------------------------------------------------------------------------
test_group_names = ['core',
'extensions', 'lib', 'terminal', 'testing', 'utils',
]
class TestSection(object):
def __init__(self, name, includes):
self.name = name
self.includes = includes
self.excludes = []
self.dependencies = []
self.enabled = True
def exclude(self, module):
if not module.startswith('IPython'):
module = self.includes[0] + "." + module
self.excludes.append(module.replace('.', os.sep))
def requires(self, *packages):
self.dependencies.extend(packages)
@property
def will_run(self):
return self.enabled and all(have[p] for p in self.dependencies)
# Name -> (include, exclude, dependencies_met)
test_sections = {n:TestSection(n, ['IPython.%s' % n]) for n in test_group_names}
# Exclusions and dependencies
# ---------------------------
# core:
sec = test_sections['core']
if not have['sqlite3']:
sec.exclude('tests.test_history')
sec.exclude('history')
if not have['matplotlib']:
sec.exclude('pylabtools'),
sec.exclude('tests.test_pylabtools')
# lib:
sec = test_sections['lib']
sec.exclude('kernel')
if not have['pygments']:
sec.exclude('tests.test_lexers')
# We do this unconditionally, so that the test suite doesn't import
# gtk, changing the default encoding and masking some unicode bugs.
sec.exclude('inputhookgtk')
# We also do this unconditionally, because wx can interfere with Unix signals.
# There are currently no tests for it anyway.
sec.exclude('inputhookwx')
# Testing inputhook will need a lot of thought, to figure out
# how to have tests that don't lock up with the gui event
# loops in the picture
sec.exclude('inputhook')
# testing:
sec = test_sections['testing']
# These have to be skipped on win32 because they use echo, rm, cd, etc.
# See ticket https://github.com/ipython/ipython/issues/87
if sys.platform == 'win32':
sec.exclude('plugin.test_exampleip')
sec.exclude('plugin.dtexample')
# don't run jupyter_console tests found via shim
test_sections['terminal'].exclude('console')
# extensions:
sec = test_sections['extensions']
# This is deprecated in favour of rpy2
sec.exclude('rmagic')
# autoreload does some strange stuff, so move it to its own test section
sec.exclude('autoreload')
sec.exclude('tests.test_autoreload')
test_sections['autoreload'] = TestSection('autoreload',
['IPython.extensions.autoreload', 'IPython.extensions.tests.test_autoreload'])
test_group_names.append('autoreload')
#-----------------------------------------------------------------------------
# Functions and classes
#-----------------------------------------------------------------------------
def check_exclusions_exist():
from IPython.paths import get_ipython_package_dir
from warnings import warn
parent = os.path.dirname(get_ipython_package_dir())
for sec in test_sections:
for pattern in sec.exclusions:
fullpath = pjoin(parent, pattern)
if not os.path.exists(fullpath) and not glob.glob(fullpath + '.*'):
warn("Excluding nonexistent file: %r" % pattern)
class ExclusionPlugin(Plugin):
"""A nose plugin to effect our exclusions of files and directories.
"""
name = 'exclusions'
score = 3000 # Should come before any other plugins
def __init__(self, exclude_patterns=None):
"""
Parameters
----------
exclude_patterns : sequence of strings, optional
Filenames containing these patterns (as raw strings, not as regular
expressions) are excluded from the tests.
"""
self.exclude_patterns = exclude_patterns or []
super(ExclusionPlugin, self).__init__()
def options(self, parser, env=os.environ):
Plugin.options(self, parser, env)
def configure(self, options, config):
Plugin.configure(self, options, config)
# Override nose trying to disable plugin.
self.enabled = True
def wantFile(self, filename):
"""Return whether the given filename should be scanned for tests.
"""
if any(pat in filename for pat in self.exclude_patterns):
return False
return None
def wantDirectory(self, directory):
"""Return whether the given directory should be scanned for tests.
"""
if any(pat in directory for pat in self.exclude_patterns):
return False
return None
class StreamCapturer(Thread):
daemon = True # Don't hang if main thread crashes
started = False
def __init__(self, echo=False):
super(StreamCapturer, self).__init__()
self.echo = echo
self.streams = []
self.buffer = BytesIO()
self.readfd, self.writefd = os.pipe()
self.buffer_lock = Lock()
self.stop = Event()
def run(self):
self.started = True
while not self.stop.is_set():
chunk = os.read(self.readfd, 1024)
with self.buffer_lock:
self.buffer.write(chunk)
if self.echo:
sys.stdout.write(decode(chunk))
os.close(self.readfd)
os.close(self.writefd)
def reset_buffer(self):
with self.buffer_lock:
self.buffer.truncate(0)
self.buffer.seek(0)
def get_buffer(self):
with self.buffer_lock:
return self.buffer.getvalue()
def ensure_started(self):
if not self.started:
self.start()
def halt(self):
"""Safely stop the thread."""
if not self.started:
return
self.stop.set()
os.write(self.writefd, b'\0') # Ensure we're not locked in a read()
self.join()
class SubprocessStreamCapturePlugin(Plugin):
name='subprocstreams'
def __init__(self):
Plugin.__init__(self)
self.stream_capturer = StreamCapturer()
self.destination = os.environ.get('IPTEST_SUBPROC_STREAMS', 'capture')
# This is ugly, but distant parts of the test machinery need to be able
# to redirect streams, so we make the object globally accessible.
nose.iptest_stdstreams_fileno = self.get_write_fileno
def get_write_fileno(self):
if self.destination == 'capture':
self.stream_capturer.ensure_started()
return self.stream_capturer.writefd
elif self.destination == 'discard':
return os.open(os.devnull, os.O_WRONLY)
else:
return sys.__stdout__.fileno()
def configure(self, options, config):
Plugin.configure(self, options, config)
# Override nose trying to disable plugin.
if self.destination == 'capture':
self.enabled = True
def startTest(self, test):
# Reset log capture
self.stream_capturer.reset_buffer()
def formatFailure(self, test, err):
# Show output
ec, ev, tb = err
captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
if captured.strip():
ev = safe_str(ev)
out = [ev, '>> begin captured subprocess output <<',
captured,
'>> end captured subprocess output <<']
return ec, '\n'.join(out), tb
return err
formatError = formatFailure
def finalize(self, result):
self.stream_capturer.halt()
def run_iptest():
"""Run the IPython test suite using nose.
This function is called when this script is **not** called with the form
`iptest all`. It simply calls nose with appropriate command line flags
and accepts all of the standard nose arguments.
"""
# Apply our monkeypatch to Xunit
if '--with-xunit' in sys.argv and not hasattr(Xunit, 'orig_addError'):
monkeypatch_xunit()
arg1 = sys.argv[1]
if arg1.startswith('IPython/'):
if arg1.endswith('.py'):
arg1 = arg1[:-3]
sys.argv[1] = arg1.replace('/', '.')
arg1 = sys.argv[1]
if arg1 in test_sections:
section = test_sections[arg1]
sys.argv[1:2] = section.includes
elif arg1.startswith('IPython.') and arg1[8:] in test_sections:
section = test_sections[arg1[8:]]
sys.argv[1:2] = section.includes
else:
section = TestSection(arg1, includes=[arg1])
argv = sys.argv + [ '--detailed-errors', # extra info in tracebacks
# We add --exe because of setuptools' imbecility (it
# blindly does chmod +x on ALL files). Nose does the
# right thing and it tries to avoid executables,
# setuptools unfortunately forces our hand here. This
# has been discussed on the distutils list and the
# setuptools devs refuse to fix this problem!
'--exe',
]
if '-a' not in argv and '-A' not in argv:
argv = argv + ['-a', '!crash']
if nose.__version__ >= '0.11':
# I don't fully understand why we need this one, but depending on what
# directory the test suite is run from, if we don't give it, 0 tests
# get run. Specifically, if the test suite is run from the source dir
# with an argument (like 'iptest.py IPython.core', 0 tests are run,
# even if the same call done in this directory works fine). It appears
# that if the requested package is in the current dir, nose bails early
# by default. Since it's otherwise harmless, leave it in by default
# for nose >= 0.11, though unfortunately nose 0.10 doesn't support it.
argv.append('--traverse-namespace')
plugins = [ ExclusionPlugin(section.excludes), KnownFailure(),
SubprocessStreamCapturePlugin() ]
# we still have some vestigial doctests in core
if (section.name.startswith(('core', 'IPython.core', 'IPython.utils'))):
plugins.append(IPythonDoctest())
argv.extend([
'--with-ipdoctest',
'--ipdoctest-tests',
'--ipdoctest-extension=txt',
])
# Use working directory set by parent process (see iptestcontroller)
if 'IPTEST_WORKING_DIR' in os.environ:
os.chdir(os.environ['IPTEST_WORKING_DIR'])
# We need a global ipython running in this process, but the special
# in-process group spawns its own IPython kernels, so for *that* group we
# must avoid also opening the global one (otherwise there's a conflict of
# singletons). Ultimately the solution to this problem is to refactor our
# assumptions about what needs to be a singleton and what doesn't (app
# objects should, individual shells shouldn't). But for now, this
# workaround allows the test suite for the inprocess module to complete.
if 'kernel.inprocess' not in section.name:
from IPython.testing import globalipapp
globalipapp.start_ipython()
# Now nose can run
TestProgram(argv=argv, addplugins=plugins)
if __name__ == '__main__':
run_iptest()

View file

@ -0,0 +1,491 @@
# -*- coding: utf-8 -*-
"""IPython Test Process Controller
This module runs one or more subprocesses which will actually run the IPython
test suite.
"""
# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.
import argparse
import multiprocessing.pool
import os
import stat
import shutil
import signal
import sys
import subprocess
import time
from .iptest import (
have, test_group_names as py_test_group_names, test_sections, StreamCapturer,
)
from IPython.utils.path import compress_user
from IPython.utils.py3compat import decode
from IPython.utils.sysinfo import get_sys_info
from IPython.utils.tempdir import TemporaryDirectory
class TestController:
"""Run tests in a subprocess
"""
#: str, IPython test suite to be executed.
section = None
#: list, command line arguments to be executed
cmd = None
#: dict, extra environment variables to set for the subprocess
env = None
#: list, TemporaryDirectory instances to clear up when the process finishes
dirs = None
#: subprocess.Popen instance
process = None
#: str, process stdout+stderr
stdout = None
def __init__(self):
self.cmd = []
self.env = {}
self.dirs = []
def setUp(self):
"""Create temporary directories etc.
This is only called when we know the test group will be run. Things
created here may be cleaned up by self.cleanup().
"""
pass
def launch(self, buffer_output=False, capture_output=False):
# print('*** ENV:', self.env) # dbg
# print('*** CMD:', self.cmd) # dbg
env = os.environ.copy()
env.update(self.env)
if buffer_output:
capture_output = True
self.stdout_capturer = c = StreamCapturer(echo=not buffer_output)
c.start()
stdout = c.writefd if capture_output else None
stderr = subprocess.STDOUT if capture_output else None
self.process = subprocess.Popen(self.cmd, stdout=stdout,
stderr=stderr, env=env)
def wait(self):
self.process.wait()
self.stdout_capturer.halt()
self.stdout = self.stdout_capturer.get_buffer()
return self.process.returncode
def cleanup_process(self):
"""Cleanup on exit by killing any leftover processes."""
subp = self.process
if subp is None or (subp.poll() is not None):
return # Process doesn't exist, or is already dead.
try:
print('Cleaning up stale PID: %d' % subp.pid)
subp.kill()
except: # (OSError, WindowsError) ?
# This is just a best effort, if we fail or the process was
# really gone, ignore it.
pass
else:
for i in range(10):
if subp.poll() is None:
time.sleep(0.1)
else:
break
if subp.poll() is None:
# The process did not die...
print('... failed. Manual cleanup may be required.')
def cleanup(self):
"Kill process if it's still alive, and clean up temporary directories"
self.cleanup_process()
for td in self.dirs:
td.cleanup()
__del__ = cleanup
class PyTestController(TestController):
"""Run Python tests using IPython.testing.iptest"""
#: str, Python command to execute in subprocess
pycmd = None
def __init__(self, section, options):
"""Create new test runner."""
TestController.__init__(self)
self.section = section
# pycmd is put into cmd[2] in PyTestController.launch()
self.cmd = [sys.executable, '-c', None, section]
self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
self.options = options
def setup(self):
ipydir = TemporaryDirectory()
self.dirs.append(ipydir)
self.env['IPYTHONDIR'] = ipydir.name
self.workingdir = workingdir = TemporaryDirectory()
self.dirs.append(workingdir)
self.env['IPTEST_WORKING_DIR'] = workingdir.name
# This means we won't get odd effects from our own matplotlib config
self.env['MPLCONFIGDIR'] = workingdir.name
# For security reasons (http://bugs.python.org/issue16202), use
# a temporary directory to which other users have no access.
self.env['TMPDIR'] = workingdir.name
# Add a non-accessible directory to PATH (see gh-7053)
noaccess = os.path.join(self.workingdir.name, "_no_access_")
self.noaccess = noaccess
os.mkdir(noaccess, 0)
PATH = os.environ.get('PATH', '')
if PATH:
PATH = noaccess + os.pathsep + PATH
else:
PATH = noaccess
self.env['PATH'] = PATH
# From options:
if self.options.xunit:
self.add_xunit()
if self.options.coverage:
self.add_coverage()
self.env['IPTEST_SUBPROC_STREAMS'] = self.options.subproc_streams
self.cmd.extend(self.options.extra_args)
def cleanup(self):
"""
Make the non-accessible directory created in setup() accessible
again, otherwise deleting the workingdir will fail.
"""
os.chmod(self.noaccess, stat.S_IRWXU)
TestController.cleanup(self)
@property
def will_run(self):
try:
return test_sections[self.section].will_run
except KeyError:
return True
def add_xunit(self):
xunit_file = os.path.abspath(self.section + '.xunit.xml')
self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
def add_coverage(self):
try:
sources = test_sections[self.section].includes
except KeyError:
sources = ['IPython']
coverage_rc = ("[run]\n"
"data_file = {data_file}\n"
"source =\n"
" {source}\n"
).format(data_file=os.path.abspath('.coverage.'+self.section),
source="\n ".join(sources))
config_file = os.path.join(self.workingdir.name, '.coveragerc')
with open(config_file, 'w') as f:
f.write(coverage_rc)
self.env['COVERAGE_PROCESS_START'] = config_file
self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
def launch(self, buffer_output=False):
self.cmd[2] = self.pycmd
super(PyTestController, self).launch(buffer_output=buffer_output)
def prepare_controllers(options):
"""Returns two lists of TestController instances, those to run, and those
not to run."""
testgroups = options.testgroups
if not testgroups:
testgroups = py_test_group_names
controllers = [PyTestController(name, options) for name in testgroups]
to_run = [c for c in controllers if c.will_run]
not_run = [c for c in controllers if not c.will_run]
return to_run, not_run
def do_run(controller, buffer_output=True):
"""Setup and run a test controller.
If buffer_output is True, no output is displayed, to avoid it appearing
interleaved. In this case, the caller is responsible for displaying test
output on failure.
Returns
-------
controller : TestController
The same controller as passed in, as a convenience for using map() type
APIs.
exitcode : int
The exit code of the test subprocess. Non-zero indicates failure.
"""
try:
try:
controller.setup()
controller.launch(buffer_output=buffer_output)
except Exception:
import traceback
traceback.print_exc()
return controller, 1 # signal failure
exitcode = controller.wait()
return controller, exitcode
except KeyboardInterrupt:
return controller, -signal.SIGINT
finally:
controller.cleanup()
def report():
"""Return a string with a summary report of test-related variables."""
inf = get_sys_info()
out = []
def _add(name, value):
out.append((name, value))
_add('IPython version', inf['ipython_version'])
_add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
_add('IPython package', compress_user(inf['ipython_path']))
_add('Python version', inf['sys_version'].replace('\n',''))
_add('sys.executable', compress_user(inf['sys_executable']))
_add('Platform', inf['platform'])
width = max(len(n) for (n,v) in out)
out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
avail = []
not_avail = []
for k, is_avail in have.items():
if is_avail:
avail.append(k)
else:
not_avail.append(k)
if avail:
out.append('\nTools and libraries available at test time:\n')
avail.sort()
out.append(' ' + ' '.join(avail)+'\n')
if not_avail:
out.append('\nTools and libraries NOT available at test time:\n')
not_avail.sort()
out.append(' ' + ' '.join(not_avail)+'\n')
return ''.join(out)
def run_iptestall(options):
"""Run the entire IPython test suite by calling nose and trial.
This function constructs :class:`IPTester` instances for all IPython
modules and package and then runs each of them. This causes the modules
and packages of IPython to be tested each in their own subprocess using
nose.
Parameters
----------
All parameters are passed as attributes of the options object.
testgroups : list of str
Run only these sections of the test suite. If empty, run all the available
sections.
fast : int or None
Run the test suite in parallel, using n simultaneous processes. If None
is passed, one process is used per CPU core. Default 1 (i.e. sequential)
inc_slow : bool
Include slow tests. By default, these tests aren't run.
url : unicode
Address:port to use when running the JS tests.
xunit : bool
Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
coverage : bool or str
Measure code coverage from tests. True will store the raw coverage data,
or pass 'html' or 'xml' to get reports.
extra_args : list
Extra arguments to pass to the test subprocesses, e.g. '-v'
"""
to_run, not_run = prepare_controllers(options)
def justify(ltext, rtext, width=70, fill='-'):
ltext += ' '
rtext = (' ' + rtext).rjust(width - len(ltext), fill)
return ltext + rtext
# Run all test runners, tracking execution time
failed = []
t_start = time.time()
print()
if options.fast == 1:
# This actually means sequential, i.e. with 1 job
for controller in to_run:
print('Test group:', controller.section)
sys.stdout.flush() # Show in correct order when output is piped
controller, res = do_run(controller, buffer_output=False)
if res:
failed.append(controller)
if res == -signal.SIGINT:
print("Interrupted")
break
print()
else:
# Run tests concurrently
try:
pool = multiprocessing.pool.ThreadPool(options.fast)
for (controller, res) in pool.imap_unordered(do_run, to_run):
res_string = 'OK' if res == 0 else 'FAILED'
print(justify('Test group: ' + controller.section, res_string))
if res:
print(decode(controller.stdout))
failed.append(controller)
if res == -signal.SIGINT:
print("Interrupted")
break
except KeyboardInterrupt:
return
for controller in not_run:
print(justify('Test group: ' + controller.section, 'NOT RUN'))
t_end = time.time()
t_tests = t_end - t_start
nrunners = len(to_run)
nfail = len(failed)
# summarize results
print('_'*70)
print('Test suite completed for system with the following information:')
print(report())
took = "Took %.3fs." % t_tests
print('Status: ', end='')
if not failed:
print('OK (%d test groups).' % nrunners, took)
else:
# If anything went wrong, point out what command to rerun manually to
# see the actual errors and individual summary
failed_sections = [c.section for c in failed]
print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
nrunners, ', '.join(failed_sections)), took)
print()
print('You may wish to rerun these, with:')
print(' iptest', *failed_sections)
print()
if options.coverage:
from coverage import coverage, CoverageException
cov = coverage(data_file='.coverage')
cov.combine()
cov.save()
# Coverage HTML report
if options.coverage == 'html':
html_dir = 'ipy_htmlcov'
shutil.rmtree(html_dir, ignore_errors=True)
print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
sys.stdout.flush()
# Custom HTML reporter to clean up module names.
from coverage.html import HtmlReporter
class CustomHtmlReporter(HtmlReporter):
def find_code_units(self, morfs):
super(CustomHtmlReporter, self).find_code_units(morfs)
for cu in self.code_units:
nameparts = cu.name.split(os.sep)
if 'IPython' not in nameparts:
continue
ix = nameparts.index('IPython')
cu.name = '.'.join(nameparts[ix:])
# Reimplement the html_report method with our custom reporter
cov.get_data()
cov.config.from_args(omit='*{0}tests{0}*'.format(os.sep), html_dir=html_dir,
html_title='IPython test coverage',
)
reporter = CustomHtmlReporter(cov, cov.config)
reporter.report(None)
print('done.')
# Coverage XML report
elif options.coverage == 'xml':
try:
cov.xml_report(outfile='ipy_coverage.xml')
except CoverageException as e:
print('Generating coverage report failed. Are you running javascript tests only?')
import traceback
traceback.print_exc()
if failed:
# Ensure that our exit code indicates failure
sys.exit(1)
argparser = argparse.ArgumentParser(description='Run IPython test suite')
argparser.add_argument('testgroups', nargs='*',
help='Run specified groups of tests. If omitted, run '
'all tests.')
argparser.add_argument('--all', action='store_true',
help='Include slow tests not run by default.')
argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
help='Run test sections in parallel. This starts as many '
'processes as you have cores, or you can specify a number.')
argparser.add_argument('--xunit', action='store_true',
help='Produce Xunit XML results')
argparser.add_argument('--coverage', nargs='?', const=True, default=False,
help="Measure test coverage. Specify 'html' or "
"'xml' to get reports.")
argparser.add_argument('--subproc-streams', default='capture',
help="What to do with stdout/stderr from subprocesses. "
"'capture' (default), 'show' and 'discard' are the options.")
def default_options():
"""Get an argparse Namespace object with the default arguments, to pass to
:func:`run_iptestall`.
"""
options = argparser.parse_args([])
options.extra_args = []
return options
def main():
# iptest doesn't work correctly if the working directory is the
# root of the IPython source tree. Tell the user to avoid
# frustration.
if os.path.exists(os.path.join(os.getcwd(),
'IPython', 'testing', '__main__.py')):
print("Don't run iptest from the IPython source directory",
file=sys.stderr)
sys.exit(1)
# Arguments after -- should be passed through to nose. Argparse treats
# everything after -- as regular positional arguments, so we separate them
# first.
try:
ix = sys.argv.index('--')
except ValueError:
to_parse = sys.argv[1:]
extra_args = []
else:
to_parse = sys.argv[1:ix]
extra_args = sys.argv[ix+1:]
options = argparser.parse_args(to_parse)
options.extra_args = extra_args
run_iptestall(options)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,178 @@
"""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 <Fernando.Perez@berkeley.edu>
"""
#-----------------------------------------------------------------------------
# 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 unittest
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.
"""
return [TestResults(f, t) for f, t in runner._name2ft.values() if f > 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()
# 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(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)
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()

View file

@ -0,0 +1,34 @@
=======================================================
Nose plugin with IPython and extension module support
=======================================================
This directory provides the key functionality for test support that IPython
needs as a nose plugin, which can be installed for use in projects other than
IPython.
The presence of a Makefile here is mostly for development and debugging
purposes as it only provides a few shorthand commands. You can manually
install the plugin by using standard Python procedures (``setup.py install``
with appropriate arguments).
To install the plugin using the Makefile, edit its first line to reflect where
you'd like the installation.
Once you've set the prefix, simply build/install the plugin with::
make
and run the tests with::
make test
You should see output similar to::
maqroll[plugin]> make test
nosetests -s --with-ipdoctest --doctest-tests dtexample.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.016s
OK

View file

@ -0,0 +1,157 @@
"""Simple example using doctests.
This file just contains doctests both using plain python and 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 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'

View file

@ -0,0 +1,761 @@
"""Nose Plugin that supports 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 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 IPython tests run via an external process the
prompt numbers may be different, and 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 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 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 IPython input and
convert any 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 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 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 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 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 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: 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 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 unconditionally 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)
with open(filename) as dh:
doc = dh.read()
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

View file

@ -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()])

View file

@ -0,0 +1,18 @@
#!/usr/bin/env python
"""A Nose plugin to support IPython doctests.
"""
from setuptools import setup
setup(name='IPython doctest plugin',
version='0.1',
author='The IPython Team',
description = 'Nose plugin to load IPython-extended doctests',
license = 'LGPL',
py_modules = ['ipdoctest'],
entry_points = {
'nose.plugins.0.10': ['ipdoctest = ipdoctest:IPythonDoctest',
'extdoctest = ipdoctest:ExtensionDoctest',
],
},
)

View file

@ -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)))

View file

@ -0,0 +1,33 @@
"""Simple example using doctests.
This file just contains doctests both using plain python and 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'

View file

@ -0,0 +1,2 @@
x = 1
print('x is:',x)

View file

@ -0,0 +1,36 @@
=======================
Combo testing example
=======================
This is a simple example that mixes ipython doctests::
In [1]: import code
In [2]: 2**12
Out[2]: 4096
with command-line example information that does *not* get executed::
$ mpirun -n 4 ipengine --controller-port=10000 --controller-ip=host0
and with literal examples of Python source code::
controller = dict(host='myhost',
engine_port=None, # default is 10105
control_port=None,
)
# keys are hostnames, values are the number of engine on that host
engines = dict(node1=2,
node2=2,
node3=2,
node3=2,
)
# Force failure to detect that this test is being run.
1/0
These source code examples are executed but no output is compared at all. An
error or failure is reported only if an exception is raised.
NOTE: the execution of pure python blocks is not yet working!

View file

@ -0,0 +1,24 @@
=====================================
Tests in example form - pure python
=====================================
This file contains doctest examples embedded as code blocks, using normal
Python prompts. See the accompanying file for similar examples using IPython
prompts (you can't mix both types within one file). The following will be run
as a test::
>>> 1+1
2
>>> print ("hello")
hello
More than one example works::
>>> s="Hello World"
>>> s.upper()
'HELLO WORLD'
but you should note that the *entire* test file is considered to be a single
test. Individual code blocks that fail are printed separately as ``example
failures``, but the whole file is still counted and reported as one test.

View file

@ -0,0 +1,30 @@
=================================
Tests in example form - IPython
=================================
You can write text files with examples that use IPython prompts (as long as you
use the nose ipython doctest plugin), but you can not mix and match prompt
styles in a single file. That is, you either use all ``>>>`` prompts or all
IPython-style prompts. Your test suite *can* have both types, you just need to
put each type of example in a separate. Using IPython prompts, you can paste
directly from your session::
In [5]: s="Hello World"
In [6]: s.upper()
Out[6]: 'HELLO WORLD'
Another example::
In [8]: 1+3
Out[8]: 4
Just like in IPython docstrings, you can use all IPython syntax and features::
In [9]: !echo "hello"
hello
In [10]: a='hi'
In [11]: !echo $a
hi

View file

@ -0,0 +1,76 @@
"""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).
"""
def doctest_simple():
"""ipdoctest must handle simple inputs
In [1]: 1
Out[1]: 1
In [2]: print(1)
1
"""
def doctest_multiline1():
"""The ipdoctest machinery must handle multiline examples gracefully.
In [2]: for i in range(4):
...: print(i)
...:
0
1
2
3
"""
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
"""

View file

@ -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 correctly 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'>]
"""

View file

@ -0,0 +1,19 @@
"""Decorators marks that a doctest should be skipped.
The IPython.testing.decorators module triggers various extra imports, including
numpy and sympy if they're present. Since this decorator is used in core parts
of IPython, it's in a separate module so that running IPython doesn't trigger
those imports."""
# Copyright (C) IPython Development Team
# Distributed under the terms of the Modified BSD License.
def skip_doctest(f):
"""Decorator - mark a function or method 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."""
f.skip_doctest = True
return f

View file

@ -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.
#-------------------------------------------------------------------------------

View file

@ -0,0 +1,164 @@
"""Tests for the decorators we've created for IPython.
"""
# Module imports
# Std lib
import inspect
import sys
# Third party
import nose.tools as nt
# Our own
from 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")

View file

@ -0,0 +1,131 @@
"""Tests for 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,
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 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 IPython.testing.ipunittest import ipdoctest, ipdocstring
#-----------------------------------------------------------------------------
# Test classes and functions
#-----------------------------------------------------------------------------
@ipdoctest
def simple_dt():
"""
>>> print(1+1)
2
"""
@ipdoctest
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
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
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
"""
def normaldt_method(self):
"""
>>> print(1+1)
2
"""

View file

@ -0,0 +1,135 @@
# encoding: utf-8
"""
Tests for testing.tools
"""
#-----------------------------------------------------------------------------
# Copyright (C) 2008-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
#-----------------------------------------------------------------------------
import os
import unittest
import nose.tools as nt
from IPython.testing import decorators as dec
from 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 = tt.temp_pyfile(src)
assert os.path.isfile(fname)
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(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)

View file

@ -0,0 +1,471 @@
"""Generic testing tools.
Authors
-------
- Fernando Perez <Fernando.Perez@berkeley.edu>
"""
# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.
import os
import re
import sys
import tempfile
import unittest
from contextlib import contextmanager
from io import StringIO
from subprocess import Popen, PIPE
from unittest.mock import patch
try:
# These tools are used by parts of the runtime, so we make the nose
# dependency optional at this point. Nose is a hard dependency to run the
# test suite, but NOT to use ipython itself.
import nose.tools as nt
has_nose = True
except ImportError:
has_nose = False
from traitlets.config.loader import Config
from IPython.utils.process import get_output_error_code
from IPython.utils.text import list_strings
from IPython.utils.io import temp_pyfile, Tee
from IPython.utils import py3compat
from . import decorators as dec
from . import skipdoctest
# The docstring for full_path doctests differently on win32 (different path
# separator) so just skip the doctest there. The example remains informative.
doctest_deco = skipdoctest.skip_doctest if sys.platform == 'win32' else dec.null_deco
@doctest_deco
def full_path(startPath,files):
"""Make full paths for all the listed files, based on startPath.
Only the base part of startPath is kept, since this routine is typically
used with a script's ``__file__`` variable as startPath. The base of startPath
is then prepended to all the listed files, forming the output list.
Parameters
----------
startPath : string
Initial path to use as the base for the results. This path is split
using os.path.split() and only its first component is kept.
files : string or list
One or more files.
Examples
--------
>>> full_path('/foo/bar.py',['a.txt','b.txt'])
['/foo/a.txt', '/foo/b.txt']
>>> full_path('/foo',['a.txt','b.txt'])
['/a.txt', '/b.txt']
If a single file is given, the output is still a list::
>>> full_path('/foo','a.txt')
['/a.txt']
"""
files = list_strings(files)
base = os.path.split(startPath)[0]
return [ os.path.join(base,f) for f in files ]
def parse_test_output(txt):
"""Parse the output of a test run and return errors, failures.
Parameters
----------
txt : str
Text output of a test run, assumed to contain a line of one of the
following forms::
'FAILED (errors=1)'
'FAILED (failures=1)'
'FAILED (errors=1, failures=1)'
Returns
-------
nerr, nfail
number of errors and failures.
"""
err_m = re.search(r'^FAILED \(errors=(\d+)\)', txt, re.MULTILINE)
if err_m:
nerr = int(err_m.group(1))
nfail = 0
return nerr, nfail
fail_m = re.search(r'^FAILED \(failures=(\d+)\)', txt, re.MULTILINE)
if fail_m:
nerr = 0
nfail = int(fail_m.group(1))
return nerr, nfail
both_m = re.search(r'^FAILED \(errors=(\d+), failures=(\d+)\)', txt,
re.MULTILINE)
if both_m:
nerr = int(both_m.group(1))
nfail = int(both_m.group(2))
return nerr, nfail
# If the input didn't match any of these forms, assume no error/failures
return 0, 0
# So nose doesn't think this is a test
parse_test_output.__test__ = False
def default_argv():
"""Return a valid default argv for creating testing instances of ipython"""
return ['--quick', # so no config file is loaded
# Other defaults to minimize side effects on stdout
'--colors=NoColor', '--no-term-title','--no-banner',
'--autocall=0']
def default_config():
"""Return a config object with good defaults for testing."""
config = Config()
config.TerminalInteractiveShell.colors = 'NoColor'
config.TerminalTerminalInteractiveShell.term_title = False,
config.TerminalInteractiveShell.autocall = 0
f = tempfile.NamedTemporaryFile(suffix=u'test_hist.sqlite', delete=False)
config.HistoryManager.hist_file = f.name
f.close()
config.HistoryManager.db_cache_size = 10000
return config
def get_ipython_cmd(as_string=False):
"""
Return appropriate IPython command line name. By default, this will return
a list that can be used with subprocess.Popen, for example, but passing
`as_string=True` allows for returning the IPython command as a string.
Parameters
----------
as_string: bool
Flag to allow to return the command as a string.
"""
ipython_cmd = [sys.executable, "-m", "IPython"]
if as_string:
ipython_cmd = " ".join(ipython_cmd)
return ipython_cmd
def ipexec(fname, options=None, commands=()):
"""Utility to call 'ipython filename'.
Starts IPython with a minimal and safe configuration to make startup as fast
as possible.
Note that this starts IPython in a subprocess!
Parameters
----------
fname : str
Name of file to be executed (should have .py or .ipy extension).
options : optional, list
Extra command-line flags to be passed to IPython.
commands : optional, list
Commands to send in on stdin
Returns
-------
``(stdout, stderr)`` of ipython subprocess.
"""
if options is None: options = []
cmdargs = default_argv() + options
test_dir = os.path.dirname(__file__)
ipython_cmd = get_ipython_cmd()
# Absolute path for filename
full_fname = os.path.join(test_dir, fname)
full_cmd = ipython_cmd + cmdargs + [full_fname]
env = os.environ.copy()
# FIXME: ignore all warnings in ipexec while we have shims
# should we keep suppressing warnings here, even after removing shims?
env['PYTHONWARNINGS'] = 'ignore'
# env.pop('PYTHONWARNINGS', None) # Avoid extraneous warnings appearing on stderr
for k, v in env.items():
# Debug a bizarre failure we've seen on Windows:
# TypeError: environment can only contain strings
if not isinstance(v, str):
print(k, v)
p = Popen(full_cmd, stdout=PIPE, stderr=PIPE, stdin=PIPE, env=env)
out, err = p.communicate(input=py3compat.encode('\n'.join(commands)) or None)
out, err = py3compat.decode(out), py3compat.decode(err)
# `import readline` causes 'ESC[?1034h' to be output sometimes,
# so strip that out before doing comparisons
if out:
out = re.sub(r'\x1b\[[^h]+h', '', out)
return out, err
def ipexec_validate(fname, expected_out, expected_err='',
options=None, commands=()):
"""Utility to call 'ipython filename' and validate output/error.
This function raises an AssertionError if the validation fails.
Note that this starts IPython in a subprocess!
Parameters
----------
fname : str
Name of the file to be executed (should have .py or .ipy extension).
expected_out : str
Expected stdout of the process.
expected_err : optional, str
Expected stderr of the process.
options : optional, list
Extra command-line flags to be passed to IPython.
Returns
-------
None
"""
import nose.tools as nt
out, err = ipexec(fname, options, commands)
#print 'OUT', out # dbg
#print 'ERR', err # dbg
# If there are any errors, we must check those before stdout, as they may be
# more informative than simply having an empty stdout.
if err:
if expected_err:
nt.assert_equal("\n".join(err.strip().splitlines()), "\n".join(expected_err.strip().splitlines()))
else:
raise ValueError('Running file %r produced error: %r' %
(fname, err))
# If no errors or output on stderr was expected, match stdout
nt.assert_equal("\n".join(out.strip().splitlines()), "\n".join(expected_out.strip().splitlines()))
class TempFileMixin(unittest.TestCase):
"""Utility class to create temporary Python/IPython files.
Meant as a mixin class for test cases."""
def mktmp(self, src, ext='.py'):
"""Make a valid python temp file."""
fname = temp_pyfile(src, ext)
if not hasattr(self, 'tmps'):
self.tmps=[]
self.tmps.append(fname)
self.fname = fname
def tearDown(self):
# If the tmpfile wasn't made because of skipped tests, like in
# win32, there's nothing to cleanup.
if hasattr(self, 'tmps'):
for fname in self.tmps:
# If the tmpfile wasn't made because of skipped tests, like in
# win32, there's nothing to cleanup.
try:
os.unlink(fname)
except:
# On Windows, even though we close the file, we still can't
# delete it. I have no clue why
pass
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self.tearDown()
pair_fail_msg = ("Testing {0}\n\n"
"In:\n"
" {1!r}\n"
"Expected:\n"
" {2!r}\n"
"Got:\n"
" {3!r}\n")
def check_pairs(func, pairs):
"""Utility function for the common case of checking a function with a
sequence of input/output pairs.
Parameters
----------
func : callable
The function to be tested. Should accept a single argument.
pairs : iterable
A list of (input, expected_output) tuples.
Returns
-------
None. Raises an AssertionError if any output does not match the expected
value.
"""
name = getattr(func, "func_name", getattr(func, "__name__", "<unknown>"))
for inp, expected in pairs:
out = func(inp)
assert out == expected, pair_fail_msg.format(name, inp, expected, out)
MyStringIO = StringIO
_re_type = type(re.compile(r''))
notprinted_msg = """Did not find {0!r} in printed output (on {1}):
-------
{2!s}
-------
"""
class AssertPrints(object):
"""Context manager for testing that code prints certain text.
Examples
--------
>>> with AssertPrints("abc", suppress=False):
... print("abcd")
... print("def")
...
abcd
def
"""
def __init__(self, s, channel='stdout', suppress=True):
self.s = s
if isinstance(self.s, (str, _re_type)):
self.s = [self.s]
self.channel = channel
self.suppress = suppress
def __enter__(self):
self.orig_stream = getattr(sys, self.channel)
self.buffer = MyStringIO()
self.tee = Tee(self.buffer, channel=self.channel)
setattr(sys, self.channel, self.buffer if self.suppress else self.tee)
def __exit__(self, etype, value, traceback):
try:
if value is not None:
# If an error was raised, don't check anything else
return False
self.tee.flush()
setattr(sys, self.channel, self.orig_stream)
printed = self.buffer.getvalue()
for s in self.s:
if isinstance(s, _re_type):
assert s.search(printed), notprinted_msg.format(s.pattern, self.channel, printed)
else:
assert s in printed, notprinted_msg.format(s, self.channel, printed)
return False
finally:
self.tee.close()
printed_msg = """Found {0!r} in printed output (on {1}):
-------
{2!s}
-------
"""
class AssertNotPrints(AssertPrints):
"""Context manager for checking that certain output *isn't* produced.
Counterpart of AssertPrints"""
def __exit__(self, etype, value, traceback):
try:
if value is not None:
# If an error was raised, don't check anything else
self.tee.close()
return False
self.tee.flush()
setattr(sys, self.channel, self.orig_stream)
printed = self.buffer.getvalue()
for s in self.s:
if isinstance(s, _re_type):
assert not s.search(printed),printed_msg.format(
s.pattern, self.channel, printed)
else:
assert s not in printed, printed_msg.format(
s, self.channel, printed)
return False
finally:
self.tee.close()
@contextmanager
def mute_warn():
from IPython.utils import warn
save_warn = warn.warn
warn.warn = lambda *a, **kw: None
try:
yield
finally:
warn.warn = save_warn
@contextmanager
def make_tempfile(name):
""" Create an empty, named, temporary file for the duration of the context.
"""
open(name, 'w').close()
try:
yield
finally:
os.unlink(name)
def fake_input(inputs):
"""Temporarily replace the input() function to return the given values
Use as a context manager:
with fake_input(['result1', 'result2']):
...
Values are returned in order. If input() is called again after the last value
was used, EOFError is raised.
"""
it = iter(inputs)
def mock_input(prompt=''):
try:
return next(it)
except StopIteration:
raise EOFError('No more inputs given')
return patch('builtins.input', mock_input)
def help_output_test(subcommand=''):
"""test that `ipython [subcommand] -h` works"""
cmd = get_ipython_cmd() + [subcommand, '-h']
out, err, rc = get_output_error_code(cmd)
nt.assert_equal(rc, 0, err)
nt.assert_not_in("Traceback", err)
nt.assert_in("Options", out)
nt.assert_in("--help-all", out)
return out, err
def help_all_output_test(subcommand=''):
"""test that `ipython [subcommand] --help-all` works"""
cmd = get_ipython_cmd() + [subcommand, '--help-all']
out, err, rc = get_output_error_code(cmd)
nt.assert_equal(rc, 0, err)
nt.assert_not_in("Traceback", err)
nt.assert_in("Options", out)
nt.assert_in("Class", out)
return out, err