From 1f517da00ec6ed4b991017b1ea88db45ff2eb6c8 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Mon, 2 Sep 2024 18:13:29 +0300 Subject: [PATCH 1/2] ENH: add ParserKlass key to DTConfig; check xdoctest.DocTestParser --- scipy_doctest/impl.py | 18 +++++++++-- scipy_doctest/tests/test_runner.py | 48 ++++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/scipy_doctest/impl.py b/scipy_doctest/impl.py index c70e99f..a346345 100644 --- a/scipy_doctest/impl.py +++ b/scipy_doctest/impl.py @@ -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, @@ -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 {} @@ -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). @@ -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) diff --git a/scipy_doctest/tests/test_runner.py b/scipy_doctest/tests/test_runner.py index 6e8e0e3..0770533 100644 --- a/scipy_doctest/tests/test_runner.py +++ b/scipy_doctest/tests/test_runner.py @@ -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) @@ -91,15 +95,53 @@ 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=''): + return self.xd.parse(string) + + +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): + 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) == 3 From 235a51e1d86600e7801dc560fbeb8d0c86284909 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Tue, 3 Sep 2024 13:25:04 +0300 Subject: [PATCH 2/2] TST: fix the xdoctest Parser drop-in example test --- scipy_doctest/tests/test_runner.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/scipy_doctest/tests/test_runner.py b/scipy_doctest/tests/test_runner.py index 0770533..e2f8a00 100644 --- a/scipy_doctest/tests/test_runner.py +++ b/scipy_doctest/tests/test_runner.py @@ -125,6 +125,15 @@ def __init__(self, config): def parse(self, string, name=''): return self.xd.parse(string) + def get_examples(self, string, name=''): + """ + 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 @@ -139,9 +148,18 @@ def test_vanilla_parser(self): @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) == 3 + 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)'