Skip to content

Commit 39597ac

Browse files
committed
Fix doctest option flags leaking between docstrings
1 parent a453d65 commit 39597ac

4 files changed

Lines changed: 92 additions & 3 deletions

File tree

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,7 @@ Pierre Sassoulas
396396
Pieter Mulder
397397
Piotr Banaszkiewicz
398398
Piotr Helm
399+
Poorva Barve
399400
Poulami Sau
400401
Prakhar Gurunani
401402
Praneeth Kodumagulla

changelog/9924.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed doctest option flags leaking between docstrings after a failure, skip, or expected failure.

src/_pytest/doctest.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -297,9 +297,14 @@ def runtest(self) -> None:
297297
_check_all_skipped(self.dtest)
298298
self._disable_output_capturing_for_darwin()
299299
failures: list[doctest.DocTestFailure] = []
300-
# Type ignored because we change the type of `out` from what
301-
# doctest expects.
302-
self.runner.run(self.dtest, out=failures) # type: ignore[arg-type]
300+
optionflags = self.runner.optionflags
301+
try:
302+
# Type ignored because we change the type of `out` from what
303+
# doctest expects.
304+
self.runner.run(self.dtest, out=failures) # type: ignore[arg-type]
305+
finally:
306+
# Our runner can raise before doctest restores its option flags.
307+
self.runner.optionflags = optionflags
303308
if failures:
304309
raise MultipleDoctestFailures(failures)
305310

testing/test_doctest.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,35 @@ def test_doctest_outcomes(self, pytester: Pytester):
272272
]
273273
)
274274

275+
@pytest.mark.parametrize("outcome", ["skip", "xfail"])
276+
@pytest.mark.parametrize("continue_on_failure", [False, True])
277+
def test_optionflags_restored_after_outcome(
278+
self, pytester: Pytester, outcome: str, continue_on_failure: bool
279+
) -> None:
280+
pytester.makepyfile(
281+
f"""
282+
import pytest
283+
284+
def first():
285+
'''
286+
>>> pytest.{outcome}("reason") # doctest: -ELLIPSIS
287+
'''
288+
289+
def second():
290+
'''
291+
>>> print("foobar")
292+
foo...
293+
'''
294+
"""
295+
)
296+
args = ["--doctest-modules"]
297+
if continue_on_failure:
298+
args.append("--doctest-continue-on-failure")
299+
result = pytester.runpytest(*args)
300+
result.assert_outcomes(
301+
passed=1, skipped=int(outcome == "skip"), xfailed=int(outcome == "xfail")
302+
)
303+
275304
def test_docstring_partial_context_around_error(self, pytester: Pytester):
276305
"""Test that we show some context before the actual line of a failing
277306
doctest.
@@ -679,6 +708,59 @@ def nice_meth(self):
679708
reprec = pytester.inline_run(p, "--doctest-modules")
680709
reprec.assertoutcome(failed=1, passed=1)
681710

711+
@pytest.mark.parametrize("continue_on_failure", [False, True])
712+
@pytest.mark.parametrize("first_example", ["0.", "1 / 0"])
713+
@pytest.mark.parametrize(
714+
"optionflags, directive, second_example, second_output, passed",
715+
[
716+
pytest.param("ELLIPSIS", "+NUMBER", "1.", "0.", 0, id="enable-number"),
717+
pytest.param(
718+
"", "+ELLIPSIS", 'print("foobar")', "foo...", 0, id="enable-ellipsis"
719+
),
720+
pytest.param(
721+
"ELLIPSIS",
722+
"-ELLIPSIS",
723+
'print("foobar")',
724+
"foo...",
725+
1,
726+
id="disable-ellipsis",
727+
),
728+
],
729+
)
730+
def test_optionflags_do_not_leak_between_docstrings(
731+
self,
732+
pytester: Pytester,
733+
continue_on_failure: bool,
734+
first_example: str,
735+
optionflags: str,
736+
directive: str,
737+
second_example: str,
738+
second_output: str,
739+
passed: int,
740+
) -> None:
741+
"""A failing docstring must not change another's comparison options (#9924)."""
742+
pytester.makeini(f"[pytest]\ndoctest_optionflags = {optionflags}")
743+
pytester.makepyfile(
744+
f"""
745+
def first():
746+
'''
747+
>>> {first_example} # doctest: {directive}
748+
2.
749+
'''
750+
751+
def second():
752+
'''
753+
>>> {second_example}
754+
{second_output}
755+
'''
756+
"""
757+
)
758+
args = ["--doctest-modules"]
759+
if continue_on_failure:
760+
args.append("--doctest-continue-on-failure")
761+
result = pytester.runpytest(*args)
762+
result.assert_outcomes(failed=2 - passed, passed=passed)
763+
682764
def test_ignored_whitespace(self, pytester: Pytester):
683765
pytester.makeini(
684766
"""

0 commit comments

Comments
 (0)