fixed subscription table
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
"""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
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# 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))
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
204
.venv/lib/python3.12/site-packages/IPython/testing/decorators.py
Normal file
204
.venv/lib/python3.12/site-packages/IPython/testing/decorators.py
Normal file
@@ -0,0 +1,204 @@
|
||||
# -*- 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
|
||||
from importlib import import_module
|
||||
|
||||
from decorator import decorator
|
||||
|
||||
# Expose the unittest-driven decorators
|
||||
from .ipunittest import ipdoctest, ipdocstring
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# 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 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.
|
||||
"""
|
||||
if msg is None:
|
||||
msg = "Test skipped due to test condition."
|
||||
|
||||
import pytest
|
||||
|
||||
assert isinstance(skip_condition, bool)
|
||||
return pytest.mark.skipif(skip_condition, reason=msg)
|
||||
|
||||
|
||||
# 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."""
|
||||
|
||||
return skipif(not 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
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# 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(
|
||||
not sys.platform.startswith("darwin"), "This test only runs under macOS"
|
||||
)
|
||||
|
||||
_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)
|
||||
|
||||
# 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')
|
||||
|
||||
# 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.
|
||||
"""
|
||||
assert (
|
||||
os.environ.get("IPTEST_WORKING_DIR", None) is None
|
||||
), "iptest deprecated since IPython 8.0"
|
||||
for cmd in commands:
|
||||
reason = f"This test runs only if command '{cmd}' is installed"
|
||||
if not shutil.which(cmd):
|
||||
import pytest
|
||||
|
||||
return pytest.mark.skip(reason=reason)
|
||||
return null_deco
|
@@ -0,0 +1,114 @@
|
||||
"""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
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from . import tools
|
||||
|
||||
from IPython.core import page
|
||||
from IPython.utils import io
|
||||
from IPython.terminal.interactiveshell import TerminalInteractiveShell
|
||||
|
||||
|
||||
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(Path(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
|
186
.venv/lib/python3.12/site-packages/IPython/testing/ipunittest.py
Normal file
186
.venv/lib/python3.12/site-packages/IPython/testing/ipunittest.py
Normal file
@@ -0,0 +1,186 @@
|
||||
"""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 sys
|
||||
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.
|
||||
"""
|
||||
if sys.version_info < (3, 13):
|
||||
return [TestResults(f, t) for f, t in runner._name2ft.values() if f > 0]
|
||||
else:
|
||||
return [
|
||||
TestResults(failure, try_)
|
||||
for failure, try_, skip in runner._stats.values()
|
||||
if failure > 0
|
||||
]
|
||||
|
||||
|
||||
class IPython2PythonConverter(object):
|
||||
"""Convert IPython 'syntax' to valid Python.
|
||||
|
||||
Eventually this code may grow to be the full IPython syntax conversion
|
||||
implementation, but for now it only does prompt conversion."""
|
||||
|
||||
def __init__(self):
|
||||
self.rps1 = re.compile(r'In\ \[\d+\]: ')
|
||||
self.rps2 = re.compile(r'\ \ \ \.\.\.+: ')
|
||||
self.rout = re.compile(r'Out\[\d+\]: \s*?\n?')
|
||||
self.pyps1 = '>>> '
|
||||
self.pyps2 = '... '
|
||||
self.rpyps1 = re.compile (r'(\s*%s)(.*)$' % self.pyps1)
|
||||
self.rpyps2 = re.compile (r'(\s*%s)(.*)$' % self.pyps2)
|
||||
|
||||
def __call__(self, ds):
|
||||
"""Convert IPython prompts to python ones in a string."""
|
||||
from . import globalipapp
|
||||
|
||||
pyps1 = '>>> '
|
||||
pyps2 = '... '
|
||||
pyout = ''
|
||||
|
||||
dnew = ds
|
||||
dnew = self.rps1.sub(pyps1, dnew)
|
||||
dnew = self.rps2.sub(pyps2, dnew)
|
||||
dnew = self.rout.sub(pyout, dnew)
|
||||
ip = InteractiveShell.instance()
|
||||
|
||||
# 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()
|
@@ -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
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,167 @@
|
||||
"""Simple example using doctests.
|
||||
|
||||
This file just contains doctests both using plain python and IPython prompts.
|
||||
All tests should be loaded by nose.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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 ipos():
|
||||
"""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
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
ipos.__skip_doctest__ = os.name == "nt"
|
||||
|
||||
|
||||
def ranfunc():
|
||||
"""A function with some random output.
|
||||
|
||||
Normal examples are verified as usual:
|
||||
>>> 1+3
|
||||
4
|
||||
|
||||
But if you put '# random' in the output, it is ignored:
|
||||
>>> 1+3
|
||||
junk goes here... # random
|
||||
|
||||
>>> 1+2
|
||||
again, anything goes #random
|
||||
if multiline, the random mark is only needed once.
|
||||
|
||||
>>> 1+2
|
||||
You can also put the random marker at the end:
|
||||
# random
|
||||
|
||||
>>> 1+2
|
||||
# random
|
||||
.. or at the beginning.
|
||||
|
||||
More correct input is properly verified:
|
||||
>>> ranfunc()
|
||||
'ranfunc'
|
||||
"""
|
||||
return 'ranfunc'
|
||||
|
||||
|
||||
def random_all():
|
||||
"""A function where we ignore the output of ALL examples.
|
||||
|
||||
Examples:
|
||||
|
||||
# all-random
|
||||
|
||||
This mark tells the testing machinery that all subsequent examples should
|
||||
be treated as random (ignoring their output). They are still executed,
|
||||
so if a they raise an error, it will be detected as such, but their
|
||||
output is completely ignored.
|
||||
|
||||
>>> 1+3
|
||||
junk goes here...
|
||||
|
||||
>>> 1+3
|
||||
klasdfj;
|
||||
|
||||
>>> 1+2
|
||||
again, anything goes
|
||||
blah...
|
||||
"""
|
||||
pass
|
||||
|
||||
def iprand():
|
||||
"""Some ipython tests with random output.
|
||||
|
||||
In [7]: 3+4
|
||||
Out[7]: 7
|
||||
|
||||
In [8]: print('hello')
|
||||
world # random
|
||||
|
||||
In [9]: iprand()
|
||||
Out[9]: 'iprand'
|
||||
"""
|
||||
return 'iprand'
|
||||
|
||||
def iprand_all():
|
||||
"""Some ipython tests with fully random output.
|
||||
|
||||
# all-random
|
||||
|
||||
In [7]: 1
|
||||
Out[7]: 99
|
||||
|
||||
In [8]: print('hello')
|
||||
world
|
||||
|
||||
In [9]: iprand_all()
|
||||
Out[9]: 'junk'
|
||||
"""
|
||||
return 'iprand_all'
|
@@ -0,0 +1,299 @@
|
||||
"""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 doctest
|
||||
import logging
|
||||
import re
|
||||
|
||||
from testpath import modified_env
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Module globals and other constants
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Classes and functions
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class DocTestFinder(doctest.DocTestFinder):
|
||||
def _get_test(self, obj, name, module, globs, source_lines):
|
||||
test = super()._get_test(obj, name, module, globs, source_lines)
|
||||
|
||||
if bool(getattr(obj, "__skip_doctest__", False)) and test is not None:
|
||||
for example in test.examples:
|
||||
example.options[doctest.SKIP] = True
|
||||
|
||||
return test
|
||||
|
||||
|
||||
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('RANDOM OK:',want, file=sys.stderr) # dbg
|
||||
return True
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
# 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 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+')
|
||||
|
||||
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
|
||||
Example = doctest.Example
|
||||
else:
|
||||
# It's an ipython example.
|
||||
terms = list(self._EXAMPLE_RE_IP.finditer(string))
|
||||
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
|
||||
|
||||
# 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):
|
||||
# Override terminal size to standardise traceback format
|
||||
with modified_env({'COLUMNS': '80', 'LINES': '24'}):
|
||||
return super(IPDocTestRunner,self).run(test,
|
||||
compileflags,out,clear_globs)
|
@@ -0,0 +1,879 @@
|
||||
# Based on Pytest doctest.py
|
||||
# Original license:
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2004-2021 Holger Krekel and others
|
||||
"""Discover and run ipdoctests in modules and test files."""
|
||||
import bdb
|
||||
import builtins
|
||||
import inspect
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
import traceback
|
||||
import types
|
||||
import warnings
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Generator,
|
||||
Iterable,
|
||||
List,
|
||||
Optional,
|
||||
Pattern,
|
||||
Sequence,
|
||||
Tuple,
|
||||
Type,
|
||||
Union,
|
||||
)
|
||||
|
||||
import pytest
|
||||
from _pytest import outcomes
|
||||
from _pytest._code.code import ExceptionInfo, ReprFileLocation, TerminalRepr
|
||||
from _pytest._io import TerminalWriter
|
||||
from _pytest.compat import safe_getattr
|
||||
from _pytest.config import Config
|
||||
from _pytest.config.argparsing import Parser
|
||||
|
||||
try:
|
||||
from _pytest.fixtures import TopRequest as FixtureRequest
|
||||
except ImportError:
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
from _pytest.nodes import Collector
|
||||
from _pytest.outcomes import OutcomeException
|
||||
from _pytest.pathlib import fnmatch_ex, import_path
|
||||
from _pytest.python_api import approx
|
||||
from _pytest.warning_types import PytestWarning
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import doctest
|
||||
|
||||
from .ipdoctest import IPDoctestOutputChecker
|
||||
|
||||
DOCTEST_REPORT_CHOICE_NONE = "none"
|
||||
DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
|
||||
DOCTEST_REPORT_CHOICE_NDIFF = "ndiff"
|
||||
DOCTEST_REPORT_CHOICE_UDIFF = "udiff"
|
||||
DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE = "only_first_failure"
|
||||
|
||||
DOCTEST_REPORT_CHOICES = (
|
||||
DOCTEST_REPORT_CHOICE_NONE,
|
||||
DOCTEST_REPORT_CHOICE_CDIFF,
|
||||
DOCTEST_REPORT_CHOICE_NDIFF,
|
||||
DOCTEST_REPORT_CHOICE_UDIFF,
|
||||
DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE,
|
||||
)
|
||||
|
||||
# Lazy definition of runner class
|
||||
RUNNER_CLASS = None
|
||||
# Lazy definition of output checker class
|
||||
CHECKER_CLASS: Optional[Type["IPDoctestOutputChecker"]] = None
|
||||
|
||||
pytest_version = tuple([int(part) for part in pytest.__version__.split(".")])
|
||||
|
||||
|
||||
def pytest_addoption(parser: Parser) -> None:
|
||||
parser.addini(
|
||||
"ipdoctest_optionflags",
|
||||
"option flags for ipdoctests",
|
||||
type="args",
|
||||
default=["ELLIPSIS"],
|
||||
)
|
||||
parser.addini(
|
||||
"ipdoctest_encoding", "encoding used for ipdoctest files", default="utf-8"
|
||||
)
|
||||
group = parser.getgroup("collect")
|
||||
group.addoption(
|
||||
"--ipdoctest-modules",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="run ipdoctests in all .py modules",
|
||||
dest="ipdoctestmodules",
|
||||
)
|
||||
group.addoption(
|
||||
"--ipdoctest-report",
|
||||
type=str.lower,
|
||||
default="udiff",
|
||||
help="choose another output format for diffs on ipdoctest failure",
|
||||
choices=DOCTEST_REPORT_CHOICES,
|
||||
dest="ipdoctestreport",
|
||||
)
|
||||
group.addoption(
|
||||
"--ipdoctest-glob",
|
||||
action="append",
|
||||
default=[],
|
||||
metavar="pat",
|
||||
help="ipdoctests file matching pattern, default: test*.txt",
|
||||
dest="ipdoctestglob",
|
||||
)
|
||||
group.addoption(
|
||||
"--ipdoctest-ignore-import-errors",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="ignore ipdoctest ImportErrors",
|
||||
dest="ipdoctest_ignore_import_errors",
|
||||
)
|
||||
group.addoption(
|
||||
"--ipdoctest-continue-on-failure",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="for a given ipdoctest, continue to run after the first failure",
|
||||
dest="ipdoctest_continue_on_failure",
|
||||
)
|
||||
|
||||
|
||||
def pytest_unconfigure() -> None:
|
||||
global RUNNER_CLASS
|
||||
|
||||
RUNNER_CLASS = None
|
||||
|
||||
|
||||
def pytest_collect_file(
|
||||
file_path: Path,
|
||||
parent: Collector,
|
||||
) -> Optional[Union["IPDoctestModule", "IPDoctestTextfile"]]:
|
||||
config = parent.config
|
||||
if file_path.suffix == ".py":
|
||||
if config.option.ipdoctestmodules and not any(
|
||||
(_is_setup_py(file_path), _is_main_py(file_path))
|
||||
):
|
||||
mod: IPDoctestModule = IPDoctestModule.from_parent(parent, path=file_path)
|
||||
return mod
|
||||
elif _is_ipdoctest(config, file_path, parent):
|
||||
txt: IPDoctestTextfile = IPDoctestTextfile.from_parent(parent, path=file_path)
|
||||
return txt
|
||||
return None
|
||||
|
||||
|
||||
if pytest_version[0] < 7:
|
||||
_collect_file = pytest_collect_file
|
||||
|
||||
def pytest_collect_file(
|
||||
path,
|
||||
parent: Collector,
|
||||
) -> Optional[Union["IPDoctestModule", "IPDoctestTextfile"]]:
|
||||
return _collect_file(Path(path), parent)
|
||||
|
||||
_import_path = import_path
|
||||
|
||||
def import_path(path, root):
|
||||
import py.path
|
||||
|
||||
return _import_path(py.path.local(path))
|
||||
|
||||
|
||||
def _is_setup_py(path: Path) -> bool:
|
||||
if path.name != "setup.py":
|
||||
return False
|
||||
contents = path.read_bytes()
|
||||
return b"setuptools" in contents or b"distutils" in contents
|
||||
|
||||
|
||||
def _is_ipdoctest(config: Config, path: Path, parent: Collector) -> bool:
|
||||
if path.suffix in (".txt", ".rst") and parent.session.isinitpath(path):
|
||||
return True
|
||||
globs = config.getoption("ipdoctestglob") or ["test*.txt"]
|
||||
return any(fnmatch_ex(glob, path) for glob in globs)
|
||||
|
||||
|
||||
def _is_main_py(path: Path) -> bool:
|
||||
return path.name == "__main__.py"
|
||||
|
||||
|
||||
class ReprFailDoctest(TerminalRepr):
|
||||
def __init__(
|
||||
self, reprlocation_lines: Sequence[Tuple[ReprFileLocation, Sequence[str]]]
|
||||
) -> None:
|
||||
self.reprlocation_lines = reprlocation_lines
|
||||
|
||||
def toterminal(self, tw: TerminalWriter) -> None:
|
||||
for reprlocation, lines in self.reprlocation_lines:
|
||||
for line in lines:
|
||||
tw.line(line)
|
||||
reprlocation.toterminal(tw)
|
||||
|
||||
|
||||
class MultipleDoctestFailures(Exception):
|
||||
def __init__(self, failures: Sequence["doctest.DocTestFailure"]) -> None:
|
||||
super().__init__()
|
||||
self.failures = failures
|
||||
|
||||
|
||||
def _init_runner_class() -> Type["IPDocTestRunner"]:
|
||||
import doctest
|
||||
from .ipdoctest import IPDocTestRunner
|
||||
|
||||
class PytestDoctestRunner(IPDocTestRunner):
|
||||
"""Runner to collect failures.
|
||||
|
||||
Note that the out variable in this case is a list instead of a
|
||||
stdout-like object.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
checker: Optional["IPDoctestOutputChecker"] = None,
|
||||
verbose: Optional[bool] = None,
|
||||
optionflags: int = 0,
|
||||
continue_on_failure: bool = True,
|
||||
) -> None:
|
||||
super().__init__(checker=checker, verbose=verbose, optionflags=optionflags)
|
||||
self.continue_on_failure = continue_on_failure
|
||||
|
||||
def report_failure(
|
||||
self,
|
||||
out,
|
||||
test: "doctest.DocTest",
|
||||
example: "doctest.Example",
|
||||
got: str,
|
||||
) -> None:
|
||||
failure = doctest.DocTestFailure(test, example, got)
|
||||
if self.continue_on_failure:
|
||||
out.append(failure)
|
||||
else:
|
||||
raise failure
|
||||
|
||||
def report_unexpected_exception(
|
||||
self,
|
||||
out,
|
||||
test: "doctest.DocTest",
|
||||
example: "doctest.Example",
|
||||
exc_info: Tuple[Type[BaseException], BaseException, types.TracebackType],
|
||||
) -> None:
|
||||
if isinstance(exc_info[1], OutcomeException):
|
||||
raise exc_info[1]
|
||||
if isinstance(exc_info[1], bdb.BdbQuit):
|
||||
outcomes.exit("Quitting debugger")
|
||||
failure = doctest.UnexpectedException(test, example, exc_info)
|
||||
if self.continue_on_failure:
|
||||
out.append(failure)
|
||||
else:
|
||||
raise failure
|
||||
|
||||
return PytestDoctestRunner
|
||||
|
||||
|
||||
def _get_runner(
|
||||
checker: Optional["IPDoctestOutputChecker"] = None,
|
||||
verbose: Optional[bool] = None,
|
||||
optionflags: int = 0,
|
||||
continue_on_failure: bool = True,
|
||||
) -> "IPDocTestRunner":
|
||||
# We need this in order to do a lazy import on doctest
|
||||
global RUNNER_CLASS
|
||||
if RUNNER_CLASS is None:
|
||||
RUNNER_CLASS = _init_runner_class()
|
||||
# Type ignored because the continue_on_failure argument is only defined on
|
||||
# PytestDoctestRunner, which is lazily defined so can't be used as a type.
|
||||
return RUNNER_CLASS( # type: ignore
|
||||
checker=checker,
|
||||
verbose=verbose,
|
||||
optionflags=optionflags,
|
||||
continue_on_failure=continue_on_failure,
|
||||
)
|
||||
|
||||
|
||||
class IPDoctestItem(pytest.Item):
|
||||
_user_ns_orig: Dict[str, Any]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
parent: "Union[IPDoctestTextfile, IPDoctestModule]",
|
||||
runner: Optional["IPDocTestRunner"] = None,
|
||||
dtest: Optional["doctest.DocTest"] = None,
|
||||
) -> None:
|
||||
super().__init__(name, parent)
|
||||
self.runner = runner
|
||||
self.dtest = dtest
|
||||
self.obj = None
|
||||
self.fixture_request: Optional[FixtureRequest] = None
|
||||
self._user_ns_orig = {}
|
||||
|
||||
@classmethod
|
||||
def from_parent( # type: ignore
|
||||
cls,
|
||||
parent: "Union[IPDoctestTextfile, IPDoctestModule]",
|
||||
*,
|
||||
name: str,
|
||||
runner: "IPDocTestRunner",
|
||||
dtest: "doctest.DocTest",
|
||||
):
|
||||
# incompatible signature due to imposed limits on subclass
|
||||
"""The public named constructor."""
|
||||
return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
|
||||
|
||||
def setup(self) -> None:
|
||||
if self.dtest is not None:
|
||||
self.fixture_request = _setup_fixtures(self)
|
||||
globs = dict(getfixture=self.fixture_request.getfixturevalue)
|
||||
for name, value in self.fixture_request.getfixturevalue(
|
||||
"ipdoctest_namespace"
|
||||
).items():
|
||||
globs[name] = value
|
||||
self.dtest.globs.update(globs)
|
||||
|
||||
from .ipdoctest import IPExample
|
||||
|
||||
if isinstance(self.dtest.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.dtest.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__"] = builtins
|
||||
self.dtest.globs = _ip.user_ns
|
||||
|
||||
def teardown(self) -> None:
|
||||
from .ipdoctest import IPExample
|
||||
|
||||
# Undo the test.globs reassignment we made
|
||||
if isinstance(self.dtest.examples[0], IPExample):
|
||||
self.dtest.globs = {}
|
||||
_ip.user_ns.clear()
|
||||
_ip.user_ns.update(self._user_ns_orig)
|
||||
del self._user_ns_orig
|
||||
|
||||
self.dtest.globs.clear()
|
||||
|
||||
def runtest(self) -> None:
|
||||
assert self.dtest is not None
|
||||
assert self.runner is not None
|
||||
_check_all_skipped(self.dtest)
|
||||
self._disable_output_capturing_for_darwin()
|
||||
failures: List["doctest.DocTestFailure"] = []
|
||||
|
||||
# exec(compile(..., "single", ...), ...) puts result in builtins._
|
||||
had_underscore_value = hasattr(builtins, "_")
|
||||
underscore_original_value = getattr(builtins, "_", None)
|
||||
|
||||
# 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()
|
||||
os.chdir(self.fspath.dirname)
|
||||
try:
|
||||
# Type ignored because we change the type of `out` from what
|
||||
# ipdoctest expects.
|
||||
self.runner.run(self.dtest, out=failures, clear_globs=False) # type: ignore[arg-type]
|
||||
finally:
|
||||
os.chdir(curdir)
|
||||
if had_underscore_value:
|
||||
setattr(builtins, "_", underscore_original_value)
|
||||
elif hasattr(builtins, "_"):
|
||||
delattr(builtins, "_")
|
||||
|
||||
if failures:
|
||||
raise MultipleDoctestFailures(failures)
|
||||
|
||||
def _disable_output_capturing_for_darwin(self) -> None:
|
||||
"""Disable output capturing. Otherwise, stdout is lost to ipdoctest (pytest#985)."""
|
||||
if platform.system() != "Darwin":
|
||||
return
|
||||
capman = self.config.pluginmanager.getplugin("capturemanager")
|
||||
if capman:
|
||||
capman.suspend_global_capture(in_=True)
|
||||
out, err = capman.read_global_capture()
|
||||
sys.stdout.write(out)
|
||||
sys.stderr.write(err)
|
||||
|
||||
# TODO: Type ignored -- breaks Liskov Substitution.
|
||||
def repr_failure( # type: ignore[override]
|
||||
self,
|
||||
excinfo: ExceptionInfo[BaseException],
|
||||
) -> Union[str, TerminalRepr]:
|
||||
import doctest
|
||||
|
||||
failures: Optional[
|
||||
Sequence[Union[doctest.DocTestFailure, doctest.UnexpectedException]]
|
||||
] = None
|
||||
if isinstance(
|
||||
excinfo.value, (doctest.DocTestFailure, doctest.UnexpectedException)
|
||||
):
|
||||
failures = [excinfo.value]
|
||||
elif isinstance(excinfo.value, MultipleDoctestFailures):
|
||||
failures = excinfo.value.failures
|
||||
|
||||
if failures is None:
|
||||
return super().repr_failure(excinfo)
|
||||
|
||||
reprlocation_lines = []
|
||||
for failure in failures:
|
||||
example = failure.example
|
||||
test = failure.test
|
||||
filename = test.filename
|
||||
if test.lineno is None:
|
||||
lineno = None
|
||||
else:
|
||||
lineno = test.lineno + example.lineno + 1
|
||||
message = type(failure).__name__
|
||||
# TODO: ReprFileLocation doesn't expect a None lineno.
|
||||
reprlocation = ReprFileLocation(filename, lineno, message) # type: ignore[arg-type]
|
||||
checker = _get_checker()
|
||||
report_choice = _get_report_choice(self.config.getoption("ipdoctestreport"))
|
||||
if lineno is not None:
|
||||
assert failure.test.docstring is not None
|
||||
lines = failure.test.docstring.splitlines(False)
|
||||
# add line numbers to the left of the error message
|
||||
assert test.lineno is not None
|
||||
lines = [
|
||||
"%03d %s" % (i + test.lineno + 1, x) for (i, x) in enumerate(lines)
|
||||
]
|
||||
# trim docstring error lines to 10
|
||||
lines = lines[max(example.lineno - 9, 0) : example.lineno + 1]
|
||||
else:
|
||||
lines = [
|
||||
"EXAMPLE LOCATION UNKNOWN, not showing all tests of that example"
|
||||
]
|
||||
indent = ">>>"
|
||||
for line in example.source.splitlines():
|
||||
lines.append(f"??? {indent} {line}")
|
||||
indent = "..."
|
||||
if isinstance(failure, doctest.DocTestFailure):
|
||||
lines += checker.output_difference(
|
||||
example, failure.got, report_choice
|
||||
).split("\n")
|
||||
else:
|
||||
inner_excinfo = ExceptionInfo.from_exc_info(failure.exc_info)
|
||||
lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)]
|
||||
lines += [
|
||||
x.strip("\n") for x in traceback.format_exception(*failure.exc_info)
|
||||
]
|
||||
reprlocation_lines.append((reprlocation, lines))
|
||||
return ReprFailDoctest(reprlocation_lines)
|
||||
|
||||
def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
|
||||
assert self.dtest is not None
|
||||
return self.path, self.dtest.lineno, "[ipdoctest] %s" % self.name
|
||||
|
||||
if pytest_version[0] < 7:
|
||||
|
||||
@property
|
||||
def path(self) -> Path:
|
||||
return Path(self.fspath)
|
||||
|
||||
|
||||
def _get_flag_lookup() -> Dict[str, int]:
|
||||
import doctest
|
||||
|
||||
return dict(
|
||||
DONT_ACCEPT_TRUE_FOR_1=doctest.DONT_ACCEPT_TRUE_FOR_1,
|
||||
DONT_ACCEPT_BLANKLINE=doctest.DONT_ACCEPT_BLANKLINE,
|
||||
NORMALIZE_WHITESPACE=doctest.NORMALIZE_WHITESPACE,
|
||||
ELLIPSIS=doctest.ELLIPSIS,
|
||||
IGNORE_EXCEPTION_DETAIL=doctest.IGNORE_EXCEPTION_DETAIL,
|
||||
COMPARISON_FLAGS=doctest.COMPARISON_FLAGS,
|
||||
ALLOW_UNICODE=_get_allow_unicode_flag(),
|
||||
ALLOW_BYTES=_get_allow_bytes_flag(),
|
||||
NUMBER=_get_number_flag(),
|
||||
)
|
||||
|
||||
|
||||
def get_optionflags(parent):
|
||||
optionflags_str = parent.config.getini("ipdoctest_optionflags")
|
||||
flag_lookup_table = _get_flag_lookup()
|
||||
flag_acc = 0
|
||||
for flag in optionflags_str:
|
||||
flag_acc |= flag_lookup_table[flag]
|
||||
return flag_acc
|
||||
|
||||
|
||||
def _get_continue_on_failure(config):
|
||||
continue_on_failure = config.getvalue("ipdoctest_continue_on_failure")
|
||||
if continue_on_failure:
|
||||
# We need to turn off this if we use pdb since we should stop at
|
||||
# the first failure.
|
||||
if config.getvalue("usepdb"):
|
||||
continue_on_failure = False
|
||||
return continue_on_failure
|
||||
|
||||
|
||||
class IPDoctestTextfile(pytest.Module):
|
||||
obj = None
|
||||
|
||||
def collect(self) -> Iterable[IPDoctestItem]:
|
||||
import doctest
|
||||
from .ipdoctest import IPDocTestParser
|
||||
|
||||
# Inspired by doctest.testfile; ideally we would use it directly,
|
||||
# but it doesn't support passing a custom checker.
|
||||
encoding = self.config.getini("ipdoctest_encoding")
|
||||
text = self.path.read_text(encoding)
|
||||
filename = str(self.path)
|
||||
name = self.path.name
|
||||
globs = {"__name__": "__main__"}
|
||||
|
||||
optionflags = get_optionflags(self)
|
||||
|
||||
runner = _get_runner(
|
||||
verbose=False,
|
||||
optionflags=optionflags,
|
||||
checker=_get_checker(),
|
||||
continue_on_failure=_get_continue_on_failure(self.config),
|
||||
)
|
||||
|
||||
parser = IPDocTestParser()
|
||||
test = parser.get_doctest(text, globs, name, filename, 0)
|
||||
if test.examples:
|
||||
yield IPDoctestItem.from_parent(
|
||||
self, name=test.name, runner=runner, dtest=test
|
||||
)
|
||||
|
||||
if pytest_version[0] < 7:
|
||||
|
||||
@property
|
||||
def path(self) -> Path:
|
||||
return Path(self.fspath)
|
||||
|
||||
@classmethod
|
||||
def from_parent(
|
||||
cls,
|
||||
parent,
|
||||
*,
|
||||
fspath=None,
|
||||
path: Optional[Path] = None,
|
||||
**kw,
|
||||
):
|
||||
if path is not None:
|
||||
import py.path
|
||||
|
||||
fspath = py.path.local(path)
|
||||
return super().from_parent(parent=parent, fspath=fspath, **kw)
|
||||
|
||||
|
||||
def _check_all_skipped(test: "doctest.DocTest") -> None:
|
||||
"""Raise pytest.skip() if all examples in the given DocTest have the SKIP
|
||||
option set."""
|
||||
import doctest
|
||||
|
||||
all_skipped = all(x.options.get(doctest.SKIP, False) for x in test.examples)
|
||||
if all_skipped:
|
||||
pytest.skip("all docstests skipped by +SKIP option")
|
||||
|
||||
|
||||
def _is_mocked(obj: object) -> bool:
|
||||
"""Return if an object is possibly a mock object by checking the
|
||||
existence of a highly improbable attribute."""
|
||||
return (
|
||||
safe_getattr(obj, "pytest_mock_example_attribute_that_shouldnt_exist", None)
|
||||
is not None
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _patch_unwrap_mock_aware() -> Generator[None, None, None]:
|
||||
"""Context manager which replaces ``inspect.unwrap`` with a version
|
||||
that's aware of mock objects and doesn't recurse into them."""
|
||||
real_unwrap = inspect.unwrap
|
||||
|
||||
def _mock_aware_unwrap(
|
||||
func: Callable[..., Any], *, stop: Optional[Callable[[Any], Any]] = None
|
||||
) -> Any:
|
||||
try:
|
||||
if stop is None or stop is _is_mocked:
|
||||
return real_unwrap(func, stop=_is_mocked)
|
||||
_stop = stop
|
||||
return real_unwrap(func, stop=lambda obj: _is_mocked(obj) or _stop(func))
|
||||
except Exception as e:
|
||||
warnings.warn(
|
||||
"Got %r when unwrapping %r. This is usually caused "
|
||||
"by a violation of Python's object protocol; see e.g. "
|
||||
"https://github.com/pytest-dev/pytest/issues/5080" % (e, func),
|
||||
PytestWarning,
|
||||
)
|
||||
raise
|
||||
|
||||
inspect.unwrap = _mock_aware_unwrap
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
inspect.unwrap = real_unwrap
|
||||
|
||||
|
||||
class IPDoctestModule(pytest.Module):
|
||||
def collect(self) -> Iterable[IPDoctestItem]:
|
||||
import doctest
|
||||
from .ipdoctest import DocTestFinder, IPDocTestParser
|
||||
|
||||
class MockAwareDocTestFinder(DocTestFinder):
|
||||
"""A hackish ipdoctest finder that overrides stdlib internals to fix a stdlib bug.
|
||||
|
||||
https://github.com/pytest-dev/pytest/issues/3456
|
||||
https://bugs.python.org/issue25532
|
||||
"""
|
||||
|
||||
def _find_lineno(self, obj, source_lines):
|
||||
"""Doctest code does not take into account `@property`, this
|
||||
is a hackish way to fix it. https://bugs.python.org/issue17446
|
||||
|
||||
Wrapped Doctests will need to be unwrapped so the correct
|
||||
line number is returned. This will be reported upstream. #8796
|
||||
"""
|
||||
if isinstance(obj, property):
|
||||
obj = getattr(obj, "fget", obj)
|
||||
|
||||
if hasattr(obj, "__wrapped__"):
|
||||
# Get the main obj in case of it being wrapped
|
||||
obj = inspect.unwrap(obj)
|
||||
|
||||
# Type ignored because this is a private function.
|
||||
return super()._find_lineno( # type:ignore[misc]
|
||||
obj,
|
||||
source_lines,
|
||||
)
|
||||
|
||||
def _find(
|
||||
self, tests, obj, name, module, source_lines, globs, seen
|
||||
) -> None:
|
||||
if _is_mocked(obj):
|
||||
return
|
||||
with _patch_unwrap_mock_aware():
|
||||
# Type ignored because this is a private function.
|
||||
super()._find( # type:ignore[misc]
|
||||
tests, obj, name, module, source_lines, globs, seen
|
||||
)
|
||||
|
||||
if self.path.name == "conftest.py":
|
||||
if pytest_version[0] < 7:
|
||||
module = self.config.pluginmanager._importconftest(
|
||||
self.path,
|
||||
self.config.getoption("importmode"),
|
||||
)
|
||||
else:
|
||||
kwargs = {"rootpath": self.config.rootpath}
|
||||
if pytest_version >= (8, 1):
|
||||
kwargs["consider_namespace_packages"] = False
|
||||
module = self.config.pluginmanager._importconftest(
|
||||
self.path,
|
||||
self.config.getoption("importmode"),
|
||||
**kwargs,
|
||||
)
|
||||
else:
|
||||
try:
|
||||
kwargs = {"root": self.config.rootpath}
|
||||
if pytest_version >= (8, 1):
|
||||
kwargs["consider_namespace_packages"] = False
|
||||
module = import_path(self.path, **kwargs)
|
||||
except ImportError:
|
||||
if self.config.getvalue("ipdoctest_ignore_import_errors"):
|
||||
pytest.skip("unable to import module %r" % self.path)
|
||||
else:
|
||||
raise
|
||||
# Uses internal doctest module parsing mechanism.
|
||||
finder = MockAwareDocTestFinder(parser=IPDocTestParser())
|
||||
optionflags = get_optionflags(self)
|
||||
runner = _get_runner(
|
||||
verbose=False,
|
||||
optionflags=optionflags,
|
||||
checker=_get_checker(),
|
||||
continue_on_failure=_get_continue_on_failure(self.config),
|
||||
)
|
||||
|
||||
for test in finder.find(module, module.__name__):
|
||||
if test.examples: # skip empty ipdoctests
|
||||
yield IPDoctestItem.from_parent(
|
||||
self, name=test.name, runner=runner, dtest=test
|
||||
)
|
||||
|
||||
if pytest_version[0] < 7:
|
||||
|
||||
@property
|
||||
def path(self) -> Path:
|
||||
return Path(self.fspath)
|
||||
|
||||
@classmethod
|
||||
def from_parent(
|
||||
cls,
|
||||
parent,
|
||||
*,
|
||||
fspath=None,
|
||||
path: Optional[Path] = None,
|
||||
**kw,
|
||||
):
|
||||
if path is not None:
|
||||
import py.path
|
||||
|
||||
fspath = py.path.local(path)
|
||||
return super().from_parent(parent=parent, fspath=fspath, **kw)
|
||||
|
||||
|
||||
def _setup_fixtures(doctest_item: IPDoctestItem) -> FixtureRequest:
|
||||
"""Used by IPDoctestTextfile and IPDoctestItem to setup fixture information."""
|
||||
|
||||
def func() -> None:
|
||||
pass
|
||||
|
||||
doctest_item.funcargs = {} # type: ignore[attr-defined]
|
||||
fm = doctest_item.session._fixturemanager
|
||||
kwargs = {"node": doctest_item, "func": func, "cls": None}
|
||||
if pytest_version <= (8, 0):
|
||||
kwargs["funcargs"] = False
|
||||
doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined]
|
||||
**kwargs
|
||||
)
|
||||
fixture_request = FixtureRequest(doctest_item, _ispytest=True)
|
||||
if pytest_version <= (8, 0):
|
||||
fixture_request._fillfixtures()
|
||||
return fixture_request
|
||||
|
||||
|
||||
def _init_checker_class() -> Type["IPDoctestOutputChecker"]:
|
||||
import doctest
|
||||
import re
|
||||
from .ipdoctest import IPDoctestOutputChecker
|
||||
|
||||
class LiteralsOutputChecker(IPDoctestOutputChecker):
|
||||
# Based on doctest_nose_plugin.py from the nltk project
|
||||
# (https://github.com/nltk/nltk) and on the "numtest" doctest extension
|
||||
# by Sebastien Boisgerault (https://github.com/boisgera/numtest).
|
||||
|
||||
_unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE)
|
||||
_bytes_literal_re = re.compile(r"(\W|^)[bB]([rR]?[\'\"])", re.UNICODE)
|
||||
_number_re = re.compile(
|
||||
r"""
|
||||
(?P<number>
|
||||
(?P<mantissa>
|
||||
(?P<integer1> [+-]?\d*)\.(?P<fraction>\d+)
|
||||
|
|
||||
(?P<integer2> [+-]?\d+)\.
|
||||
)
|
||||
(?:
|
||||
[Ee]
|
||||
(?P<exponent1> [+-]?\d+)
|
||||
)?
|
||||
|
|
||||
(?P<integer3> [+-]?\d+)
|
||||
(?:
|
||||
[Ee]
|
||||
(?P<exponent2> [+-]?\d+)
|
||||
)
|
||||
)
|
||||
""",
|
||||
re.VERBOSE,
|
||||
)
|
||||
|
||||
def check_output(self, want: str, got: str, optionflags: int) -> bool:
|
||||
if super().check_output(want, got, optionflags):
|
||||
return True
|
||||
|
||||
allow_unicode = optionflags & _get_allow_unicode_flag()
|
||||
allow_bytes = optionflags & _get_allow_bytes_flag()
|
||||
allow_number = optionflags & _get_number_flag()
|
||||
|
||||
if not allow_unicode and not allow_bytes and not allow_number:
|
||||
return False
|
||||
|
||||
def remove_prefixes(regex: Pattern[str], txt: str) -> str:
|
||||
return re.sub(regex, r"\1\2", txt)
|
||||
|
||||
if allow_unicode:
|
||||
want = remove_prefixes(self._unicode_literal_re, want)
|
||||
got = remove_prefixes(self._unicode_literal_re, got)
|
||||
|
||||
if allow_bytes:
|
||||
want = remove_prefixes(self._bytes_literal_re, want)
|
||||
got = remove_prefixes(self._bytes_literal_re, got)
|
||||
|
||||
if allow_number:
|
||||
got = self._remove_unwanted_precision(want, got)
|
||||
|
||||
return super().check_output(want, got, optionflags)
|
||||
|
||||
def _remove_unwanted_precision(self, want: str, got: str) -> str:
|
||||
wants = list(self._number_re.finditer(want))
|
||||
gots = list(self._number_re.finditer(got))
|
||||
if len(wants) != len(gots):
|
||||
return got
|
||||
offset = 0
|
||||
for w, g in zip(wants, gots):
|
||||
fraction: Optional[str] = w.group("fraction")
|
||||
exponent: Optional[str] = w.group("exponent1")
|
||||
if exponent is None:
|
||||
exponent = w.group("exponent2")
|
||||
precision = 0 if fraction is None else len(fraction)
|
||||
if exponent is not None:
|
||||
precision -= int(exponent)
|
||||
if float(w.group()) == approx(float(g.group()), abs=10**-precision):
|
||||
# They're close enough. Replace the text we actually
|
||||
# got with the text we want, so that it will match when we
|
||||
# check the string literally.
|
||||
got = (
|
||||
got[: g.start() + offset] + w.group() + got[g.end() + offset :]
|
||||
)
|
||||
offset += w.end() - w.start() - (g.end() - g.start())
|
||||
return got
|
||||
|
||||
return LiteralsOutputChecker
|
||||
|
||||
|
||||
def _get_checker() -> "IPDoctestOutputChecker":
|
||||
"""Return a IPDoctestOutputChecker subclass that supports some
|
||||
additional options:
|
||||
|
||||
* ALLOW_UNICODE and ALLOW_BYTES options to ignore u'' and b''
|
||||
prefixes (respectively) in string literals. Useful when the same
|
||||
ipdoctest should run in Python 2 and Python 3.
|
||||
|
||||
* NUMBER to ignore floating-point differences smaller than the
|
||||
precision of the literal number in the ipdoctest.
|
||||
|
||||
An inner class is used to avoid importing "ipdoctest" at the module
|
||||
level.
|
||||
"""
|
||||
global CHECKER_CLASS
|
||||
if CHECKER_CLASS is None:
|
||||
CHECKER_CLASS = _init_checker_class()
|
||||
return CHECKER_CLASS()
|
||||
|
||||
|
||||
def _get_allow_unicode_flag() -> int:
|
||||
"""Register and return the ALLOW_UNICODE flag."""
|
||||
import doctest
|
||||
|
||||
return doctest.register_optionflag("ALLOW_UNICODE")
|
||||
|
||||
|
||||
def _get_allow_bytes_flag() -> int:
|
||||
"""Register and return the ALLOW_BYTES flag."""
|
||||
import doctest
|
||||
|
||||
return doctest.register_optionflag("ALLOW_BYTES")
|
||||
|
||||
|
||||
def _get_number_flag() -> int:
|
||||
"""Register and return the NUMBER flag."""
|
||||
import doctest
|
||||
|
||||
return doctest.register_optionflag("NUMBER")
|
||||
|
||||
|
||||
def _get_report_choice(key: str) -> int:
|
||||
"""Return the actual `ipdoctest` module flag value.
|
||||
|
||||
We want to do it as late as possible to avoid importing `ipdoctest` and all
|
||||
its dependencies when parsing options, as it adds overhead and breaks tests.
|
||||
"""
|
||||
import doctest
|
||||
|
||||
return {
|
||||
DOCTEST_REPORT_CHOICE_UDIFF: doctest.REPORT_UDIFF,
|
||||
DOCTEST_REPORT_CHOICE_CDIFF: doctest.REPORT_CDIFF,
|
||||
DOCTEST_REPORT_CHOICE_NDIFF: doctest.REPORT_NDIFF,
|
||||
DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE: doctest.REPORT_ONLY_FIRST_FAILURE,
|
||||
DOCTEST_REPORT_CHOICE_NONE: 0,
|
||||
}[key]
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def ipdoctest_namespace() -> Dict[str, Any]:
|
||||
"""Fixture that returns a :py:class:`dict` that will be injected into the
|
||||
namespace of ipdoctests."""
|
||||
return dict()
|
@@ -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',
|
||||
],
|
||||
},
|
||||
)
|
@@ -0,0 +1,44 @@
|
||||
"""Simple example using doctests.
|
||||
|
||||
This file just contains doctests both using plain python and IPython prompts.
|
||||
All tests should be loaded by Pytest.
|
||||
"""
|
||||
|
||||
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 ipyfunc():
|
||||
"""Some IPython tests...
|
||||
|
||||
In [1]: ipyfunc()
|
||||
Out[1]: 'ipyfunc'
|
||||
|
||||
In [2]: import os
|
||||
|
||||
In [3]: 2+3
|
||||
Out[3]: 5
|
||||
|
||||
In [4]: for i in range(3):
|
||||
...: print(i, end=' ')
|
||||
...: print(i+1, end=' ')
|
||||
...:
|
||||
Out[4]: 0 1 1 2 2 3
|
||||
"""
|
||||
return "ipyfunc"
|
@@ -0,0 +1,2 @@
|
||||
x = 1
|
||||
print("x is:", x)
|
@@ -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!
|
@@ -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.
|
@@ -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
|
@@ -0,0 +1,92 @@
|
||||
"""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
|
||||
"""
|
||||
|
||||
|
||||
def doctest_builtin_underscore():
|
||||
"""Defining builtins._ should not break anything outside the doctest
|
||||
while also should be working as expected inside the doctest.
|
||||
|
||||
In [1]: import builtins
|
||||
|
||||
In [2]: builtins._ = 42
|
||||
|
||||
In [3]: builtins._
|
||||
Out[3]: 42
|
||||
|
||||
In [4]: _
|
||||
Out[4]: 42
|
||||
"""
|
@@ -0,0 +1,39 @@
|
||||
"""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
|
||||
"""
|
@@ -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
|
@@ -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.
|
||||
# -------------------------------------------------------------------------------
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,168 @@
|
||||
"""Tests for the decorators we've created for IPython.
|
||||
"""
|
||||
|
||||
# Module imports
|
||||
# Std lib
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
# Our own
|
||||
from IPython.testing import decorators as dec
|
||||
from IPython.testing.skipdoctest import skip_doctest
|
||||
from IPython.utils.text import dedent
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# 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.
|
||||
@skip_doctest
|
||||
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__
|
||||
|
||||
assert dedent(check) == dedent(val), "doctest_bad docstrings don't match"
|
||||
|
||||
|
||||
# Doctest skipping should work for class methods too
|
||||
class FooClass(object):
|
||||
"""FooClass
|
||||
|
||||
Example:
|
||||
|
||||
>>> 1+1
|
||||
2
|
||||
"""
|
||||
|
||||
@skip_doctest
|
||||
def __init__(self,x):
|
||||
"""Make a FooClass.
|
||||
|
||||
Example:
|
||||
|
||||
>>> f = FooClass(3)
|
||||
junk
|
||||
"""
|
||||
print('Making a FooClass.')
|
||||
self.x = x
|
||||
|
||||
@skip_doctest
|
||||
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():
|
||||
assert sys.platform.startswith("linux") is False, "This test can't run under linux"
|
||||
|
||||
|
||||
@dec.skip_win32
|
||||
def test_win32():
|
||||
assert sys.platform != "win32", "This test can't run under windows"
|
||||
|
||||
|
||||
@dec.skip_osx
|
||||
def test_osx():
|
||||
assert sys.platform != "darwin", "This test can't run under osx"
|
@@ -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
|
||||
"""
|
@@ -0,0 +1,133 @@
|
||||
# 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
|
||||
|
||||
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"])
|
||||
assert result, ["/foo/a.txt" == "/foo/b.txt"]
|
||||
spath = "/foo"
|
||||
result = tt.full_path(spath, ["a.txt", "b.txt"])
|
||||
assert result, ["/a.txt" == "/b.txt"]
|
||||
result = tt.full_path(spath, ["a.txt"])
|
||||
assert 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"])
|
||||
assert result, ["c:\\foo\\a.txt" == "c:\\foo\\b.txt"]
|
||||
spath = "c:\\foo"
|
||||
result = tt.full_path(spath, ["a.txt", "b.txt"])
|
||||
assert result, ["c:\\a.txt" == "c:\\b.txt"]
|
||||
result = tt.full_path(spath, ["a.txt"])
|
||||
assert 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)
|
||||
assert nerr == nerr1
|
||||
assert nfail == nfail1
|
||||
|
||||
|
||||
def test_temp_pyfile():
|
||||
src = 'pass\n'
|
||||
fname = tt.temp_pyfile(src)
|
||||
assert os.path.isfile(fname)
|
||||
with open(fname, encoding="utf-8") as fh2:
|
||||
src2 = fh2.read()
|
||||
assert 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)
|
471
.venv/lib/python3.12/site-packages/IPython/testing/tools.py
Normal file
471
.venv/lib/python3.12/site-packages/IPython/testing/tools.py
Normal 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
|
||||
from pathlib import Path
|
||||
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
|
||||
|
||||
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: str, files: list[str]) -> list[str]:
|
||||
"""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 : 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']
|
||||
|
||||
"""
|
||||
assert isinstance(files, list)
|
||||
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 = Path(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, Path
|
||||
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.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
|
||||
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
|
||||
# Prevent coloring under PyCharm ("\x1b[0m" at the end of the stdout)
|
||||
env.pop("PYCHARM_HOSTED", None)
|
||||
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, Path
|
||||
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
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
|
||||
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:
|
||||
assert "\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
|
||||
assert "\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.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
|
||||
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):
|
||||
__tracebackhide__ = True
|
||||
|
||||
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):
|
||||
__tracebackhide__ = True
|
||||
|
||||
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", encoding="utf-8").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 as e:
|
||||
raise EOFError('No more inputs given') from e
|
||||
|
||||
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)
|
||||
assert rc == 0, err
|
||||
assert "Traceback" not in err
|
||||
assert "Options" in out
|
||||
assert "--help-all" in 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)
|
||||
assert rc == 0, err
|
||||
assert "Traceback" not in err
|
||||
assert "Options" in out
|
||||
assert "Class" in out
|
||||
return out, err
|
||||
|
Reference in New Issue
Block a user