Skip to content

Commit 1984c10

Browse files
committed
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.
1 parent 3246d8a commit 1984c10

File tree

2 files changed

+65
-37
lines changed

2 files changed

+65
-37
lines changed

src/_pytest/doctest.py

Lines changed: 61 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@
66
import traceback
77
import warnings
88
from contextlib import contextmanager
9+
from typing import Dict
10+
from typing import List
11+
from typing import Optional
912
from typing import Sequence
1013
from typing import Tuple
14+
from typing import Union
1115

1216
import pytest
1317
from _pytest import outcomes
@@ -20,6 +24,10 @@
2024
from _pytest.python_api import approx
2125
from _pytest.warning_types import PytestWarning
2226

27+
if False: # TYPE_CHECKING
28+
import doctest
29+
from typing import Type
30+
2331
DOCTEST_REPORT_CHOICE_NONE = "none"
2432
DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
2533
DOCTEST_REPORT_CHOICE_NDIFF = "ndiff"
@@ -36,6 +44,8 @@
3644

3745
# Lazy definition of runner class
3846
RUNNER_CLASS = None
47+
# Lazy definition of output checker class
48+
CHECKER_CLASS = None # type: Optional[Type[doctest.OutputChecker]]
3949

4050

4151
def pytest_addoption(parser):
@@ -139,7 +149,7 @@ def __init__(self, failures):
139149
self.failures = failures
140150

141151

142-
def _init_runner_class():
152+
def _init_runner_class() -> "Type[doctest.DocTestRunner]":
143153
import doctest
144154

145155
class PytestDoctestRunner(doctest.DebugRunner):
@@ -177,12 +187,19 @@ def report_unexpected_exception(self, out, test, example, exc_info):
177187
return PytestDoctestRunner
178188

179189

180-
def _get_runner(checker=None, verbose=None, optionflags=0, continue_on_failure=True):
190+
def _get_runner(
191+
checker: Optional["doctest.OutputChecker"] = None,
192+
verbose: Optional[bool] = None,
193+
optionflags: int = 0,
194+
continue_on_failure: bool = True,
195+
) -> "doctest.DocTestRunner":
181196
# We need this in order to do a lazy import on doctest
182197
global RUNNER_CLASS
183198
if RUNNER_CLASS is None:
184199
RUNNER_CLASS = _init_runner_class()
185-
return RUNNER_CLASS(
200+
# Type ignored because the continue_on_failure argument is only defined on
201+
# PytestDoctestRunner, which is lazily defined so can't be used as a type.
202+
return RUNNER_CLASS( # type: ignore
186203
checker=checker,
187204
verbose=verbose,
188205
optionflags=optionflags,
@@ -211,7 +228,7 @@ def setup(self):
211228
def runtest(self):
212229
_check_all_skipped(self.dtest)
213230
self._disable_output_capturing_for_darwin()
214-
failures = []
231+
failures = [] # type: List[doctest.DocTestFailure]
215232
self.runner.run(self.dtest, out=failures)
216233
if failures:
217234
raise MultipleDoctestFailures(failures)
@@ -232,7 +249,9 @@ def _disable_output_capturing_for_darwin(self):
232249
def repr_failure(self, excinfo):
233250
import doctest
234251

235-
failures = None
252+
failures = (
253+
None
254+
) # type: Optional[List[Union[doctest.DocTestFailure, doctest.UnexpectedException]]]
236255
if excinfo.errisinstance((doctest.DocTestFailure, doctest.UnexpectedException)):
237256
failures = [excinfo.value]
238257
elif excinfo.errisinstance(MultipleDoctestFailures):
@@ -255,8 +274,10 @@ def repr_failure(self, excinfo):
255274
self.config.getoption("doctestreport")
256275
)
257276
if lineno is not None:
277+
assert failure.test.docstring is not None
258278
lines = failure.test.docstring.splitlines(False)
259279
# add line numbers to the left of the error message
280+
assert test.lineno is not None
260281
lines = [
261282
"%03d %s" % (i + test.lineno + 1, x)
262283
for (i, x) in enumerate(lines)
@@ -288,7 +309,7 @@ def reportinfo(self):
288309
return self.fspath, self.dtest.lineno, "[doctest] %s" % self.name
289310

290311

291-
def _get_flag_lookup():
312+
def _get_flag_lookup() -> Dict[str, int]:
292313
import doctest
293314

294315
return dict(
@@ -340,14 +361,16 @@ def collect(self):
340361
optionflags = get_optionflags(self)
341362

342363
runner = _get_runner(
343-
verbose=0,
364+
verbose=False,
344365
optionflags=optionflags,
345366
checker=_get_checker(),
346367
continue_on_failure=_get_continue_on_failure(self.config),
347368
)
348369

349370
parser = doctest.DocTestParser()
350-
test = parser.get_doctest(text, globs, name, filename, 0)
371+
# Remove ignore once this reaches mypy:
372+
# https://github.com/python/typeshed/commit/3e4a251b2b6da6bb43137acf5abf81ecfa7ba8ee
373+
test = parser.get_doctest(text, globs, name, filename, 0) # type: ignore
351374
if test.examples:
352375
yield DoctestItem(test.name, self, runner, test)
353376

@@ -419,7 +442,8 @@ def _find(self, tests, obj, name, module, source_lines, globs, seen):
419442
return
420443
with _patch_unwrap_mock_aware():
421444

422-
doctest.DocTestFinder._find(
445+
# Type ignored because this is a private function.
446+
doctest.DocTestFinder._find( # type: ignore
423447
self, tests, obj, name, module, source_lines, globs, seen
424448
)
425449

@@ -437,7 +461,7 @@ def _find(self, tests, obj, name, module, source_lines, globs, seen):
437461
finder = MockAwareDocTestFinder()
438462
optionflags = get_optionflags(self)
439463
runner = _get_runner(
440-
verbose=0,
464+
verbose=False,
441465
optionflags=optionflags,
442466
checker=_get_checker(),
443467
continue_on_failure=_get_continue_on_failure(self.config),
@@ -466,24 +490,7 @@ def func():
466490
return fixture_request
467491

468492

469-
def _get_checker():
470-
"""
471-
Returns a doctest.OutputChecker subclass that supports some
472-
additional options:
473-
474-
* ALLOW_UNICODE and ALLOW_BYTES options to ignore u'' and b''
475-
prefixes (respectively) in string literals. Useful when the same
476-
doctest should run in Python 2 and Python 3.
477-
478-
* NUMBER to ignore floating-point differences smaller than the
479-
precision of the literal number in the doctest.
480-
481-
An inner class is used to avoid importing "doctest" at the module
482-
level.
483-
"""
484-
if hasattr(_get_checker, "LiteralsOutputChecker"):
485-
return _get_checker.LiteralsOutputChecker()
486-
493+
def _init_checker_class() -> "Type[doctest.OutputChecker]":
487494
import doctest
488495
import re
489496

@@ -573,11 +580,31 @@ def _remove_unwanted_precision(self, want, got):
573580
offset += w.end() - w.start() - (g.end() - g.start())
574581
return got
575582

576-
_get_checker.LiteralsOutputChecker = LiteralsOutputChecker
577-
return _get_checker.LiteralsOutputChecker()
583+
return LiteralsOutputChecker
584+
585+
586+
def _get_checker() -> "doctest.OutputChecker":
587+
"""
588+
Returns a doctest.OutputChecker subclass that supports some
589+
additional options:
590+
591+
* ALLOW_UNICODE and ALLOW_BYTES options to ignore u'' and b''
592+
prefixes (respectively) in string literals. Useful when the same
593+
doctest should run in Python 2 and Python 3.
594+
595+
* NUMBER to ignore floating-point differences smaller than the
596+
precision of the literal number in the doctest.
597+
598+
An inner class is used to avoid importing "doctest" at the module
599+
level.
600+
"""
601+
global CHECKER_CLASS
602+
if CHECKER_CLASS is None:
603+
CHECKER_CLASS = _init_checker_class()
604+
return CHECKER_CLASS()
578605

579606

580-
def _get_allow_unicode_flag():
607+
def _get_allow_unicode_flag() -> int:
581608
"""
582609
Registers and returns the ALLOW_UNICODE flag.
583610
"""
@@ -586,7 +613,7 @@ def _get_allow_unicode_flag():
586613
return doctest.register_optionflag("ALLOW_UNICODE")
587614

588615

589-
def _get_allow_bytes_flag():
616+
def _get_allow_bytes_flag() -> int:
590617
"""
591618
Registers and returns the ALLOW_BYTES flag.
592619
"""
@@ -595,7 +622,7 @@ def _get_allow_bytes_flag():
595622
return doctest.register_optionflag("ALLOW_BYTES")
596623

597624

598-
def _get_number_flag():
625+
def _get_number_flag() -> int:
599626
"""
600627
Registers and returns the NUMBER flag.
601628
"""
@@ -604,7 +631,7 @@ def _get_number_flag():
604631
return doctest.register_optionflag("NUMBER")
605632

606633

607-
def _get_report_choice(key):
634+
def _get_report_choice(key: str) -> int:
608635
"""
609636
This function returns the actual `doctest` module flag value, we want to do it as late as possible to avoid
610637
importing `doctest` and all its dependencies when parsing options, as it adds overhead and breaks tests.

testing/test_doctest.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -839,7 +839,8 @@ def test_bytes_literal(self, testdir):
839839
reprec = testdir.inline_run()
840840
reprec.assertoutcome(failed=1)
841841

842-
def test_number_re(self):
842+
def test_number_re(self) -> None:
843+
_number_re = _get_checker()._number_re # type: ignore
843844
for s in [
844845
"1.",
845846
"+1.",
@@ -861,12 +862,12 @@ def test_number_re(self):
861862
"-1.2e-3",
862863
]:
863864
print(s)
864-
m = _get_checker()._number_re.match(s)
865+
m = _number_re.match(s)
865866
assert m is not None
866867
assert float(m.group()) == pytest.approx(float(s))
867868
for s in ["1", "abc"]:
868869
print(s)
869-
assert _get_checker()._number_re.match(s) is None
870+
assert _number_re.match(s) is None
870871

871872
@pytest.mark.parametrize("config_mode", ["ini", "comment"])
872873
def test_number_precision(self, testdir, config_mode):

0 commit comments

Comments
 (0)