Fix check_untyped_defs errors in doctest
In order to make the LiteralOutputChecker lazy initialization more amenable to type checking, I changed it to match the scheme already used in this file to lazy-initialize PytestDoctestRunner.
This commit is contained in:
parent
3246d8a6e9
commit
1984c10427
|
@ -6,8 +6,12 @@ import sys
|
||||||
import traceback
|
import traceback
|
||||||
import warnings
|
import warnings
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
from typing import Dict
|
||||||
|
from typing import List
|
||||||
|
from typing import Optional
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest import outcomes
|
from _pytest import outcomes
|
||||||
|
@ -20,6 +24,10 @@ from _pytest.outcomes import Skipped
|
||||||
from _pytest.python_api import approx
|
from _pytest.python_api import approx
|
||||||
from _pytest.warning_types import PytestWarning
|
from _pytest.warning_types import PytestWarning
|
||||||
|
|
||||||
|
if False: # TYPE_CHECKING
|
||||||
|
import doctest
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
DOCTEST_REPORT_CHOICE_NONE = "none"
|
DOCTEST_REPORT_CHOICE_NONE = "none"
|
||||||
DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
|
DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
|
||||||
DOCTEST_REPORT_CHOICE_NDIFF = "ndiff"
|
DOCTEST_REPORT_CHOICE_NDIFF = "ndiff"
|
||||||
|
@ -36,6 +44,8 @@ DOCTEST_REPORT_CHOICES = (
|
||||||
|
|
||||||
# Lazy definition of runner class
|
# Lazy definition of runner class
|
||||||
RUNNER_CLASS = None
|
RUNNER_CLASS = None
|
||||||
|
# Lazy definition of output checker class
|
||||||
|
CHECKER_CLASS = None # type: Optional[Type[doctest.OutputChecker]]
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser):
|
def pytest_addoption(parser):
|
||||||
|
@ -139,7 +149,7 @@ class MultipleDoctestFailures(Exception):
|
||||||
self.failures = failures
|
self.failures = failures
|
||||||
|
|
||||||
|
|
||||||
def _init_runner_class():
|
def _init_runner_class() -> "Type[doctest.DocTestRunner]":
|
||||||
import doctest
|
import doctest
|
||||||
|
|
||||||
class PytestDoctestRunner(doctest.DebugRunner):
|
class PytestDoctestRunner(doctest.DebugRunner):
|
||||||
|
@ -177,12 +187,19 @@ def _init_runner_class():
|
||||||
return PytestDoctestRunner
|
return PytestDoctestRunner
|
||||||
|
|
||||||
|
|
||||||
def _get_runner(checker=None, verbose=None, optionflags=0, continue_on_failure=True):
|
def _get_runner(
|
||||||
|
checker: Optional["doctest.OutputChecker"] = None,
|
||||||
|
verbose: Optional[bool] = None,
|
||||||
|
optionflags: int = 0,
|
||||||
|
continue_on_failure: bool = True,
|
||||||
|
) -> "doctest.DocTestRunner":
|
||||||
# We need this in order to do a lazy import on doctest
|
# We need this in order to do a lazy import on doctest
|
||||||
global RUNNER_CLASS
|
global RUNNER_CLASS
|
||||||
if RUNNER_CLASS is None:
|
if RUNNER_CLASS is None:
|
||||||
RUNNER_CLASS = _init_runner_class()
|
RUNNER_CLASS = _init_runner_class()
|
||||||
return 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,
|
checker=checker,
|
||||||
verbose=verbose,
|
verbose=verbose,
|
||||||
optionflags=optionflags,
|
optionflags=optionflags,
|
||||||
|
@ -211,7 +228,7 @@ class DoctestItem(pytest.Item):
|
||||||
def runtest(self):
|
def runtest(self):
|
||||||
_check_all_skipped(self.dtest)
|
_check_all_skipped(self.dtest)
|
||||||
self._disable_output_capturing_for_darwin()
|
self._disable_output_capturing_for_darwin()
|
||||||
failures = []
|
failures = [] # type: List[doctest.DocTestFailure]
|
||||||
self.runner.run(self.dtest, out=failures)
|
self.runner.run(self.dtest, out=failures)
|
||||||
if failures:
|
if failures:
|
||||||
raise MultipleDoctestFailures(failures)
|
raise MultipleDoctestFailures(failures)
|
||||||
|
@ -232,7 +249,9 @@ class DoctestItem(pytest.Item):
|
||||||
def repr_failure(self, excinfo):
|
def repr_failure(self, excinfo):
|
||||||
import doctest
|
import doctest
|
||||||
|
|
||||||
failures = None
|
failures = (
|
||||||
|
None
|
||||||
|
) # type: Optional[List[Union[doctest.DocTestFailure, doctest.UnexpectedException]]]
|
||||||
if excinfo.errisinstance((doctest.DocTestFailure, doctest.UnexpectedException)):
|
if excinfo.errisinstance((doctest.DocTestFailure, doctest.UnexpectedException)):
|
||||||
failures = [excinfo.value]
|
failures = [excinfo.value]
|
||||||
elif excinfo.errisinstance(MultipleDoctestFailures):
|
elif excinfo.errisinstance(MultipleDoctestFailures):
|
||||||
|
@ -255,8 +274,10 @@ class DoctestItem(pytest.Item):
|
||||||
self.config.getoption("doctestreport")
|
self.config.getoption("doctestreport")
|
||||||
)
|
)
|
||||||
if lineno is not None:
|
if lineno is not None:
|
||||||
|
assert failure.test.docstring is not None
|
||||||
lines = failure.test.docstring.splitlines(False)
|
lines = failure.test.docstring.splitlines(False)
|
||||||
# add line numbers to the left of the error message
|
# add line numbers to the left of the error message
|
||||||
|
assert test.lineno is not None
|
||||||
lines = [
|
lines = [
|
||||||
"%03d %s" % (i + test.lineno + 1, x)
|
"%03d %s" % (i + test.lineno + 1, x)
|
||||||
for (i, x) in enumerate(lines)
|
for (i, x) in enumerate(lines)
|
||||||
|
@ -288,7 +309,7 @@ class DoctestItem(pytest.Item):
|
||||||
return self.fspath, self.dtest.lineno, "[doctest] %s" % self.name
|
return self.fspath, self.dtest.lineno, "[doctest] %s" % self.name
|
||||||
|
|
||||||
|
|
||||||
def _get_flag_lookup():
|
def _get_flag_lookup() -> Dict[str, int]:
|
||||||
import doctest
|
import doctest
|
||||||
|
|
||||||
return dict(
|
return dict(
|
||||||
|
@ -340,14 +361,16 @@ class DoctestTextfile(pytest.Module):
|
||||||
optionflags = get_optionflags(self)
|
optionflags = get_optionflags(self)
|
||||||
|
|
||||||
runner = _get_runner(
|
runner = _get_runner(
|
||||||
verbose=0,
|
verbose=False,
|
||||||
optionflags=optionflags,
|
optionflags=optionflags,
|
||||||
checker=_get_checker(),
|
checker=_get_checker(),
|
||||||
continue_on_failure=_get_continue_on_failure(self.config),
|
continue_on_failure=_get_continue_on_failure(self.config),
|
||||||
)
|
)
|
||||||
|
|
||||||
parser = doctest.DocTestParser()
|
parser = doctest.DocTestParser()
|
||||||
test = parser.get_doctest(text, globs, name, filename, 0)
|
# Remove ignore once this reaches mypy:
|
||||||
|
# https://github.com/python/typeshed/commit/3e4a251b2b6da6bb43137acf5abf81ecfa7ba8ee
|
||||||
|
test = parser.get_doctest(text, globs, name, filename, 0) # type: ignore
|
||||||
if test.examples:
|
if test.examples:
|
||||||
yield DoctestItem(test.name, self, runner, test)
|
yield DoctestItem(test.name, self, runner, test)
|
||||||
|
|
||||||
|
@ -419,7 +442,8 @@ class DoctestModule(pytest.Module):
|
||||||
return
|
return
|
||||||
with _patch_unwrap_mock_aware():
|
with _patch_unwrap_mock_aware():
|
||||||
|
|
||||||
doctest.DocTestFinder._find(
|
# Type ignored because this is a private function.
|
||||||
|
doctest.DocTestFinder._find( # type: ignore
|
||||||
self, tests, obj, name, module, source_lines, globs, seen
|
self, tests, obj, name, module, source_lines, globs, seen
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -437,7 +461,7 @@ class DoctestModule(pytest.Module):
|
||||||
finder = MockAwareDocTestFinder()
|
finder = MockAwareDocTestFinder()
|
||||||
optionflags = get_optionflags(self)
|
optionflags = get_optionflags(self)
|
||||||
runner = _get_runner(
|
runner = _get_runner(
|
||||||
verbose=0,
|
verbose=False,
|
||||||
optionflags=optionflags,
|
optionflags=optionflags,
|
||||||
checker=_get_checker(),
|
checker=_get_checker(),
|
||||||
continue_on_failure=_get_continue_on_failure(self.config),
|
continue_on_failure=_get_continue_on_failure(self.config),
|
||||||
|
@ -466,24 +490,7 @@ def _setup_fixtures(doctest_item):
|
||||||
return fixture_request
|
return fixture_request
|
||||||
|
|
||||||
|
|
||||||
def _get_checker():
|
def _init_checker_class() -> "Type[doctest.OutputChecker]":
|
||||||
"""
|
|
||||||
Returns a doctest.OutputChecker 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
|
|
||||||
doctest should run in Python 2 and Python 3.
|
|
||||||
|
|
||||||
* NUMBER to ignore floating-point differences smaller than the
|
|
||||||
precision of the literal number in the doctest.
|
|
||||||
|
|
||||||
An inner class is used to avoid importing "doctest" at the module
|
|
||||||
level.
|
|
||||||
"""
|
|
||||||
if hasattr(_get_checker, "LiteralsOutputChecker"):
|
|
||||||
return _get_checker.LiteralsOutputChecker()
|
|
||||||
|
|
||||||
import doctest
|
import doctest
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
@ -573,11 +580,31 @@ def _get_checker():
|
||||||
offset += w.end() - w.start() - (g.end() - g.start())
|
offset += w.end() - w.start() - (g.end() - g.start())
|
||||||
return got
|
return got
|
||||||
|
|
||||||
_get_checker.LiteralsOutputChecker = LiteralsOutputChecker
|
return LiteralsOutputChecker
|
||||||
return _get_checker.LiteralsOutputChecker()
|
|
||||||
|
|
||||||
|
|
||||||
def _get_allow_unicode_flag():
|
def _get_checker() -> "doctest.OutputChecker":
|
||||||
|
"""
|
||||||
|
Returns a doctest.OutputChecker 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
|
||||||
|
doctest should run in Python 2 and Python 3.
|
||||||
|
|
||||||
|
* NUMBER to ignore floating-point differences smaller than the
|
||||||
|
precision of the literal number in the doctest.
|
||||||
|
|
||||||
|
An inner class is used to avoid importing "doctest" 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:
|
||||||
"""
|
"""
|
||||||
Registers and returns the ALLOW_UNICODE flag.
|
Registers and returns the ALLOW_UNICODE flag.
|
||||||
"""
|
"""
|
||||||
|
@ -586,7 +613,7 @@ def _get_allow_unicode_flag():
|
||||||
return doctest.register_optionflag("ALLOW_UNICODE")
|
return doctest.register_optionflag("ALLOW_UNICODE")
|
||||||
|
|
||||||
|
|
||||||
def _get_allow_bytes_flag():
|
def _get_allow_bytes_flag() -> int:
|
||||||
"""
|
"""
|
||||||
Registers and returns the ALLOW_BYTES flag.
|
Registers and returns the ALLOW_BYTES flag.
|
||||||
"""
|
"""
|
||||||
|
@ -595,7 +622,7 @@ def _get_allow_bytes_flag():
|
||||||
return doctest.register_optionflag("ALLOW_BYTES")
|
return doctest.register_optionflag("ALLOW_BYTES")
|
||||||
|
|
||||||
|
|
||||||
def _get_number_flag():
|
def _get_number_flag() -> int:
|
||||||
"""
|
"""
|
||||||
Registers and returns the NUMBER flag.
|
Registers and returns the NUMBER flag.
|
||||||
"""
|
"""
|
||||||
|
@ -604,7 +631,7 @@ def _get_number_flag():
|
||||||
return doctest.register_optionflag("NUMBER")
|
return doctest.register_optionflag("NUMBER")
|
||||||
|
|
||||||
|
|
||||||
def _get_report_choice(key):
|
def _get_report_choice(key: str) -> int:
|
||||||
"""
|
"""
|
||||||
This function returns the actual `doctest` module flag value, we want to do it as late as possible to avoid
|
This function returns the actual `doctest` module flag value, we want to do it as late as possible to avoid
|
||||||
importing `doctest` and all its dependencies when parsing options, as it adds overhead and breaks tests.
|
importing `doctest` and all its dependencies when parsing options, as it adds overhead and breaks tests.
|
||||||
|
|
|
@ -839,7 +839,8 @@ class TestLiterals:
|
||||||
reprec = testdir.inline_run()
|
reprec = testdir.inline_run()
|
||||||
reprec.assertoutcome(failed=1)
|
reprec.assertoutcome(failed=1)
|
||||||
|
|
||||||
def test_number_re(self):
|
def test_number_re(self) -> None:
|
||||||
|
_number_re = _get_checker()._number_re # type: ignore
|
||||||
for s in [
|
for s in [
|
||||||
"1.",
|
"1.",
|
||||||
"+1.",
|
"+1.",
|
||||||
|
@ -861,12 +862,12 @@ class TestLiterals:
|
||||||
"-1.2e-3",
|
"-1.2e-3",
|
||||||
]:
|
]:
|
||||||
print(s)
|
print(s)
|
||||||
m = _get_checker()._number_re.match(s)
|
m = _number_re.match(s)
|
||||||
assert m is not None
|
assert m is not None
|
||||||
assert float(m.group()) == pytest.approx(float(s))
|
assert float(m.group()) == pytest.approx(float(s))
|
||||||
for s in ["1", "abc"]:
|
for s in ["1", "abc"]:
|
||||||
print(s)
|
print(s)
|
||||||
assert _get_checker()._number_re.match(s) is None
|
assert _number_re.match(s) is None
|
||||||
|
|
||||||
@pytest.mark.parametrize("config_mode", ["ini", "comment"])
|
@pytest.mark.parametrize("config_mode", ["ini", "comment"])
|
||||||
def test_number_precision(self, testdir, config_mode):
|
def test_number_precision(self, testdir, config_mode):
|
||||||
|
|
Loading…
Reference in New Issue