1
1
"""Sphinx test suite utilities"""
2
+
2
3
from __future__ import annotations
3
4
4
5
import contextlib
5
6
import os
6
7
import re
7
8
import sys
8
9
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
10
13
from xml .etree import ElementTree
11
14
12
15
from docutils import nodes
18
21
from sphinx .util .docutils import additional_nodes
19
22
20
23
if TYPE_CHECKING :
21
- from io import StringIO
24
+ from collections . abc import Mapping
22
25
from pathlib import Path
26
+ from typing import Any
23
27
24
28
from docutils .nodes import Node
25
29
@@ -73,29 +77,74 @@ def etree_parse(path: str) -> Any:
73
77
74
78
75
79
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.
79
99
"""
80
100
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
83
103
84
104
def __init__ (
85
105
self ,
106
+ / , # to allow 'self' as an extras
86
107
buildername : str = 'html' ,
87
108
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 ,
93
114
tags : list [str ] | None = None ,
94
- docutils_conf : str | None = None ,
115
+ docutils_conf : str | None = None , # extra constructor argument
95
116
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 ,
96
123
) -> None :
97
124
assert srcdir is not None
98
125
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
+
99
148
self .docutils_conf_path = srcdir / 'docutils.conf'
100
149
if docutils_conf is not None :
101
150
self .docutils_conf_path .write_text (docutils_conf , encoding = 'utf8' )
@@ -112,17 +161,35 @@ def __init__(
112
161
confoverrides = {}
113
162
114
163
self ._saved_path = sys .path .copy ()
164
+ self .extras : Mapping [str , Any ] = MappingProxyType (extras )
165
+ """Extras keyword arguments."""
115
166
116
167
try :
117
168
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 ,
121
174
)
122
175
except Exception :
123
176
self .cleanup ()
124
177
raise
125
178
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
+
126
193
def cleanup (self , doctrees : bool = False ) -> None :
127
194
sys .path [:] = self ._saved_path
128
195
_clean_up_global_state ()
0 commit comments