Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: add ParserKlass key to DTConfig; check xdoctest.DocTestParser #174

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions scipy_doctest/impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,16 @@ class DTConfig:
instance.
This class will be instantiated by ``DTRunner``.
Defaults to `DTChecker`.
ParserKlass : object, optional
The class for the Parser object. Must mimic the ``DTParser`` API:
subclass the `doctest.DocTestParser` and make the constructor signature
be ``__init__(self, config=None)``, where ``config`` is a ``DTConfig``
instance.
This class will be instantiated by ``DTRunner`` via ``DTFinder``.
Defaults to ``DTParser``.

"""
def __init__(self, *, # DTChecker configuration
CheckerKlass=None,
default_namespace=None,
check_namespace=None,
rndm_markers=None,
Expand All @@ -129,9 +135,11 @@ def __init__(self, *, # DTChecker configuration
pytest_extra_ignore=None,
pytest_extra_skip=None,
pytest_extra_xfail=None,
# plug-and-play configuration
CheckerKlass=None,
ParserKlass=None,
):
### DTChecker configuration ###
self.CheckerKlass = CheckerKlass or DTChecker

# The namespace to run examples in
self.default_namespace = default_namespace or {}
Expand Down Expand Up @@ -217,6 +225,10 @@ def __init__(self, *, # DTChecker configuration
self.pytest_extra_skip = pytest_extra_skip or {}
self.pytest_extra_xfail = pytest_extra_xfail or {}

### Plug-and-play: Checker/Parser classes
self.CheckerKlass = CheckerKlass or DTChecker
self.ParserKlass = ParserKlass or DTParser


def try_convert_namedtuple(got):
# suppose that "got" is smth like MoodResult(statistic=10, pvalue=0.1).
Expand Down Expand Up @@ -495,7 +507,7 @@ def __init__(self, verbose=None, parser=None, recurse=True,
config = DTConfig()
self.config = config
if parser is None:
parser = DTParser(config)
parser = config.ParserKlass(config)
verbose, dtverbose = util._map_verbosity(verbose)
super().__init__(dtverbose, parser, recurse, exclude_empty)

Expand Down
66 changes: 63 additions & 3 deletions scipy_doctest/tests/test_runner.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import io

import doctest

import pytest

try:
import xdoctest
HAVE_XDOCTEST = True
except ModuleNotFoundError:
HAVE_XDOCTEST = False

from . import (failure_cases as module,
finder_cases as finder_module,
module_cases)
Expand Down Expand Up @@ -91,15 +95,71 @@ class VanillaOutputChecker(doctest.OutputChecker):
def __init__(self, config):
pass


class TestCheckerDropIn:
"""Test DTChecker and vanilla doctest OutputChecker being drop-in replacements.
"""
def test_vanilla_checker(self):
config = DTConfig(CheckerKlass=VanillaOutputChecker)
runner = DebugDTRunner(config=config)
tests = DTFinder().find(module_cases.func)
tests = DTFinder(config=config).find(module_cases.func)

with pytest.raises(doctest.DocTestFailure):
for t in tests:
runner.run(t)


class VanillaParser(doctest.DocTestParser):
def __init__(self, config):
self.config = config
pass


class XDParser(doctest.DocTestParser):
""" Wrap `xdoctest` parser.
"""
def __init__(self, config):
self.config = config
self.xd = xdoctest.parser.DoctestParser()

def parse(self, string, name='<string>'):
return self.xd.parse(string)

def get_examples(self, string, name='<string>'):
"""
Similar to doctest.DocTestParser.get_examples, only
account for the fact that individual examples
are instances of DoctestPart not doctest.Example
"""
return [x for x in self.parse(string, name)
if isinstance(x, xdoctest.doctest_part.DoctestPart)]


class TestParserDropIn:
""" Test an alternative DoctestParser
"""
def test_vanilla_parser(self):
config = DTConfig(ParserKlass=VanillaParser)
runner = DebugDTRunner(config=config)
tests = DTFinder(config=config).find(module_cases.func3)

assert len(tests) == 1
assert len(tests[0].examples) == 3

@pytest.mark.skipif(not HAVE_XDOCTEST, reason="needs xdoctest")
def test_xdoctest_parser(self):
# Note that the # of examples differ from DTParser:
# - xdoctest groups doctest lines with no 'want' output into a single
# example.
# - "examples" here are DoctestPart instances, which _almost_ quack
# like `doctest.Example` but not completely.
config = DTConfig(ParserKlass=XDParser)
runner = DebugDTRunner(config=config)
tests = DTFinder(config=config).find(module_cases.func3)

assert len(tests) == 1
assert len(tests[0].examples) == 2
assert (tests[0].examples[0].source ==
'import numpy as np\na = np.array([1, 2, 3, 4]) / 3'
)
assert tests[0].examples[1].source == 'print(a)'