Skip to content

Commit 4ca034b

Browse files
authored
[tests] start fixing the current plugin [part 1] (#12089)
1 parent 6af611e commit 4ca034b

File tree

3 files changed

+105
-22
lines changed

3 files changed

+105
-22
lines changed

CHANGES.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ Deprecated
1313
* #11693: Support for old-style :file:`Makefile` and :file:`make.bat` output
1414
in :program:`sphinx-quickstart`, and the associated options :option:`!-M`,
1515
:option:`!-m`, :option:`!--no-use-make-mode`, and :option:`!--use-make-mode`.
16+
* #11285: Direct access to :attr:`!sphinx.testing.util.SphinxTestApp._status`
17+
or :attr:`!sphinx.testing.util.SphinxTestApp._warning` is deprecated. Use
18+
the public properties :attr:`!sphinx.testing.util.SphinxTestApp.status`
19+
and :attr:`!sphinx.testing.util.SphinxTestApp.warning` instead.
20+
Patch by Bénédikt Tran.
1621

1722
Features added
1823
--------------
@@ -101,6 +106,13 @@ Bugs fixed
101106

102107
Testing
103108
-------
109+
* #11285: :func:`!pytest.mark.sphinx` and :class:`!sphinx.testing.util.SphinxTestApp`
110+
accept *warningiserror*, *keep_going* and *verbosity* as keyword arguments.
111+
Patch by Bénédikt Tran.
112+
* #11285: :class:`!sphinx.testing.util.SphinxTestApp` *status* and *warning*
113+
arguments are checked to be :class:`io.StringIO` objects (the public API
114+
incorrectly assumed this without checking it).
115+
Patch by Bénédikt Tran.
104116

105117
* pytest: report the result of ``test_run_epubcheck`` as ``skipped`` instead of
106118
``success`` when Java and/or the ``epubcheck.jar`` code are not available.

sphinx/testing/fixtures.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@
2020

2121
DEFAULT_ENABLED_MARKERS = [
2222
(
23-
'sphinx(builder, testroot=None, freshenv=False, confoverrides=None, tags=None, '
24-
'docutils_conf=None, parallel=0): arguments to initialize the sphinx test application.'
23+
'sphinx('
24+
'buildername="html", /, *, '
25+
'testroot="root", confoverrides=None, freshenv=False, '
26+
'warningiserror=False, tags=None, verbosity=0, parallel=0, '
27+
'keep_going=False, builddir=None, docutils_conf=None'
28+
'): arguments to initialize the sphinx test application.'
2529
),
2630
'test_params(shared_result=...): test parameters.',
2731
]
@@ -45,8 +49,8 @@ def store(self, key: str, app_: SphinxTestApp) -> Any:
4549
if key in self.cache:
4650
return
4751
data = {
48-
'status': app_._status.getvalue(),
49-
'warning': app_._warning.getvalue(),
52+
'status': app_.status.getvalue(),
53+
'warning': app_.warning.getvalue(),
5054
}
5155
self.cache[key] = data
5256

@@ -163,15 +167,15 @@ def status(app: SphinxTestApp) -> StringIO:
163167
"""
164168
Back-compatibility for testing with previous @with_app decorator
165169
"""
166-
return app._status
170+
return app.status
167171

168172

169173
@pytest.fixture()
170174
def warning(app: SphinxTestApp) -> StringIO:
171175
"""
172176
Back-compatibility for testing with previous @with_app decorator
173177
"""
174-
return app._warning
178+
return app.warning
175179

176180

177181
@pytest.fixture()

sphinx/testing/util.py

Lines changed: 83 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
"""Sphinx test suite utilities"""
2+
23
from __future__ import annotations
34

45
import contextlib
56
import os
67
import re
78
import sys
89
import warnings
9-
from typing import IO, TYPE_CHECKING, Any
10+
from io import StringIO
11+
from types import MappingProxyType
12+
from typing import TYPE_CHECKING
1013
from xml.etree import ElementTree
1114

1215
from docutils import nodes
@@ -18,8 +21,9 @@
1821
from sphinx.util.docutils import additional_nodes
1922

2023
if TYPE_CHECKING:
21-
from io import StringIO
24+
from collections.abc import Mapping
2225
from pathlib import Path
26+
from typing import Any
2327

2428
from docutils.nodes import Node
2529

@@ -73,29 +77,74 @@ def etree_parse(path: str) -> Any:
7377

7478

7579
class SphinxTestApp(sphinx.application.Sphinx):
76-
"""
77-
A subclass of :class:`Sphinx` that runs on the test root, with some
78-
better default values for the initialization parameters.
80+
"""A subclass of :class:`~sphinx.application.Sphinx` for tests.
81+
82+
The constructor uses some better default values for the initialization
83+
parameters and supports arbitrary keywords stored in the :attr:`extras`
84+
read-only mapping.
85+
86+
It is recommended to use::
87+
88+
@pytest.mark.sphinx('html')
89+
def test(app):
90+
app = ...
91+
92+
instead of::
93+
94+
def test():
95+
app = SphinxTestApp('html', srcdir=srcdir)
96+
97+
In the former case, the 'app' fixture takes care of setting the source
98+
directory, whereas in the latter, the user must provide it themselves.
7999
"""
80100

81-
_status: StringIO
82-
_warning: StringIO
101+
# see https://github.com/sphinx-doc/sphinx/pull/12089 for the
102+
# discussion on how the signature of this class should be used
83103

84104
def __init__(
85105
self,
106+
/, # to allow 'self' as an extras
86107
buildername: str = 'html',
87108
srcdir: Path | None = None,
88-
builddir: Path | None = None,
89-
freshenv: bool = False,
90-
confoverrides: dict | None = None,
91-
status: IO | None = None,
92-
warning: IO | None = None,
109+
builddir: Path | None = None, # extra constructor argument
110+
freshenv: bool = False, # argument is not in the same order as in the superclass
111+
confoverrides: dict[str, Any] | None = None,
112+
status: StringIO | None = None,
113+
warning: StringIO | None = None,
93114
tags: list[str] | None = None,
94-
docutils_conf: str | None = None,
115+
docutils_conf: str | None = None, # extra constructor argument
95116
parallel: int = 0,
117+
# additional arguments at the end to keep the signature
118+
verbosity: int = 0, # argument is not in the same order as in the superclass
119+
keep_going: bool = False,
120+
warningiserror: bool = False, # argument is not in the same order as in the superclass
121+
# unknown keyword arguments
122+
**extras: Any,
96123
) -> None:
97124
assert srcdir is not None
98125

126+
if verbosity == -1:
127+
quiet = True
128+
verbosity = 0
129+
else:
130+
quiet = False
131+
132+
if status is None:
133+
# ensure that :attr:`status` is a StringIO and not sys.stdout
134+
# but allow the stream to be /dev/null by passing verbosity=-1
135+
status = None if quiet else StringIO()
136+
elif not isinstance(status, StringIO):
137+
err = "%r must be an io.StringIO object, got: %s" % ('status', type(status))
138+
raise TypeError(err)
139+
140+
if warning is None:
141+
# ensure that :attr:`warning` is a StringIO and not sys.stderr
142+
# but allow the stream to be /dev/null by passing verbosity=-1
143+
warning = None if quiet else StringIO()
144+
elif not isinstance(warning, StringIO):
145+
err = '%r must be an io.StringIO object, got: %s' % ('warning', type(warning))
146+
raise TypeError(err)
147+
99148
self.docutils_conf_path = srcdir / 'docutils.conf'
100149
if docutils_conf is not None:
101150
self.docutils_conf_path.write_text(docutils_conf, encoding='utf8')
@@ -112,17 +161,35 @@ def __init__(
112161
confoverrides = {}
113162

114163
self._saved_path = sys.path.copy()
164+
self.extras: Mapping[str, Any] = MappingProxyType(extras)
165+
"""Extras keyword arguments."""
115166

116167
try:
117168
super().__init__(
118-
srcdir, confdir, outdir, doctreedir,
119-
buildername, confoverrides, status, warning, freshenv,
120-
warningiserror=False, tags=tags, parallel=parallel,
169+
srcdir, confdir, outdir, doctreedir, buildername,
170+
confoverrides=confoverrides, status=status, warning=warning,
171+
freshenv=freshenv, warningiserror=warningiserror, tags=tags,
172+
verbosity=verbosity, parallel=parallel, keep_going=keep_going,
173+
pdb=False,
121174
)
122175
except Exception:
123176
self.cleanup()
124177
raise
125178

179+
@property
180+
def status(self) -> StringIO:
181+
"""The in-memory text I/O for the application status messages."""
182+
# sphinx.application.Sphinx uses StringIO for a quiet stream
183+
assert isinstance(self._status, StringIO)
184+
return self._status
185+
186+
@property
187+
def warning(self) -> StringIO:
188+
"""The in-memory text I/O for the application warning messages."""
189+
# sphinx.application.Sphinx uses StringIO for a quiet stream
190+
assert isinstance(self._warning, StringIO)
191+
return self._warning
192+
126193
def cleanup(self, doctrees: bool = False) -> None:
127194
sys.path[:] = self._saved_path
128195
_clean_up_global_state()

0 commit comments

Comments
 (0)