first commit

This commit is contained in:
Ayxan
2022-05-23 00:16:32 +04:00
commit d660f2a4ca
24786 changed files with 4428337 additions and 0 deletions

View File

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

View File

@ -0,0 +1,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'

View File

@ -0,0 +1,300 @@
"""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 os
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 >> sys.stderr, 'RANDOM OK:',want # 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)

View File

@ -0,0 +1,860 @@
# 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 builtins
import bdb
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 Any
from typing import Callable
from typing import Dict
from typing import Generator
from typing import Iterable
from typing import List
from typing import Optional
from typing import Pattern
from typing import Sequence
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import Union
import pytest
from _pytest import outcomes
from _pytest._code.code import ExceptionInfo
from _pytest._code.code import ReprFileLocation
from _pytest._code.code import TerminalRepr
from _pytest._io import TerminalWriter
from _pytest.compat import safe_getattr
from _pytest.config import Config
from _pytest.config.argparsing import Parser
from _pytest.fixtures import FixtureRequest
from _pytest.nodes import Collector
from _pytest.outcomes import OutcomeException
from _pytest.pathlib import fnmatch_ex
from _pytest.pathlib import import_path
from _pytest.python_api import approx
from _pytest.warning_types import PytestWarning
if TYPE_CHECKING:
import doctest
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
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 int(pytest.__version__.split(".")[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):
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
@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 int(pytest.__version__.split(".")[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 int(pytest.__version__.split(".")[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 int(pytest.__version__.split(".")[0]) < 7:
module = self.config.pluginmanager._importconftest(
self.path,
self.config.getoption("importmode"),
)
else:
module = self.config.pluginmanager._importconftest(
self.path,
self.config.getoption("importmode"),
rootpath=self.config.rootpath,
)
else:
try:
module = import_path(self.path, root=self.config.rootpath)
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 int(pytest.__version__.split(".")[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
doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined]
node=doctest_item, func=func, cls=None, funcargs=False
)
fixture_request = FixtureRequest(doctest_item, _ispytest=True)
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()

View File

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

View File

@ -0,0 +1,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"

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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
"""

View File

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