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: make OutputChecker pluggable #141

Merged
merged 2 commits into from
Mar 20, 2024
Merged
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
5 changes: 4 additions & 1 deletion scpdt/impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ class DTConfig:
a string. If not empty, the string value is used as the skip reason.
"""
def __init__(self, *, # DTChecker configuration
CheckerKlass=None,
default_namespace=None,
check_namespace=None,
rndm_markers=None,
Expand All @@ -108,6 +109,8 @@ def __init__(self, *, # DTChecker configuration
pytest_extra_xfail=None,
):
### DTChecker configuration ###
self.CheckerKlass = CheckerKlass or DTChecker

# The namespace to run examples in
self.default_namespace = default_namespace or {}

Expand Down Expand Up @@ -340,7 +343,7 @@ def __init__(self, checker=None, verbose=None, optionflags=None, config=None):
if config is None:
config = DTConfig()
if checker is None:
checker = DTChecker(config)
checker = config.CheckerKlass(config)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great idea. However, the implementation currently does not make the Checker pluggable since doctest.OutputChecker has no __init__ function and can therefore not take any config argument like DTChecker.
Any attempt to use the Vanilla OutputChecker will fail:

    checker = config.CheckerKlass(config)
E   TypeError: OutputChecker() takes no arguments

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is OutputChecker here, is it doctest.OutputChecker? Then indeed, it'll fail. ISTM the best we can do is to document the story: "Any admissible OutputChecker must have __init__(self, config)".

self.nameerror_after_exception = config.nameerror_after_exception
if optionflags is None:
optionflags = config.optionflags
Expand Down
10 changes: 4 additions & 6 deletions scpdt/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,6 @@ def collect(self):
runner = _get_runner(self.config,
verbose=False,
optionflags=optionflags,
checker=DTChecker(config=self.config.dt_config)
)

# strategy='api': discover doctests in public, non-deprecated objects in module
Expand All @@ -228,7 +227,7 @@ def collect(self):
yield pydoctest.DoctestItem.from_parent(
self, name=test.name, runner=runner, dtest=test
)


class DTTextfile(DoctestTextfile):
"""
Expand All @@ -253,7 +252,6 @@ def collect(self):
runner = _get_runner(self.config,
verbose=False,
optionflags=optionflags,
checker=DTChecker(config=self.config.dt_config)
)

# Plug in an instance of `DTParser` which parses the doctest examples from the text file and
Expand All @@ -268,7 +266,7 @@ def collect(self):
)


def _get_runner(config, checker, verbose, optionflags):
def _get_runner(config, verbose, optionflags):
"""
Override function to return an instance of PytestDTRunner.

Expand Down Expand Up @@ -321,5 +319,5 @@ def report_unexpected_exception(self, out, test, example, exc_info):
out.append(failure)
else:
raise failure
return PytestDTRunner(checker=checker, verbose=verbose, optionflags=optionflags, config=config.dt_config)

return PytestDTRunner(verbose=verbose, optionflags=optionflags, config=config.dt_config)
35 changes: 35 additions & 0 deletions scpdt/tests/test_pytest_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,38 @@ def test_local_file_cases(pytester):
python_file = PosixPath(path_str)
result = pytester.inline_run(python_file, "--doctest-modules")
assert result.ret == pytest.ExitCode.OK


def test_alt_checker(pytester):
"""Test an alternative Checker."""

# create a temporary conftest.py file
pytester.makeconftest(
"""
import doctest
from scpdt.conftest import dt_config

class Vanilla(doctest.OutputChecker):
def __init__(self, config):
pass

dt_config.CheckerKlass = Vanilla
"""
)

# create a temporary pytest test file
f = pytester.makepyfile(
"""
def func():
'''
>>> 2 / 3 # fails with vanilla doctest.OutputChecker
0.667
'''
pass
"""
)

# run all tests with pytest
result = pytester.inline_run(f, '--doctest-modules')
assert result.ret == pytest.ExitCode.TESTS_FAILED

28 changes: 26 additions & 2 deletions scpdt/tests/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

import pytest

from . import failure_cases as module, finder_cases as finder_module
from .. import DTFinder, DTRunner, DebugDTRunner
from . import (failure_cases as module,
finder_cases as finder_module,
module_cases)
from .. import DTFinder, DTRunner, DebugDTRunner, DTConfig


### Smoke test DTRunner methods. Mainly to check that they are runnable.
Expand Down Expand Up @@ -79,3 +81,25 @@ def test_debug_runner_exception(self):
# exception carries the original test
assert orig_exception.test is tests[0]


class VanillaOutputChecker(doctest.OutputChecker):
"""doctest.OutputChecker to drop in for DTChecker.

LSP break: OutputChecker does not have __init__,
here we add it to agree with DTChecker.
"""
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)

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

Loading