Coverage for pyquickhelper/helpgen/sphinxm_mock_app.py: 81%
189 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-03 02:21 +0200
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-03 02:21 +0200
1"""
2@file
3@brief Helpers to convert docstring to various format.
4"""
5import logging
6import warnings
7from docutils.parsers.rst.directives import directive as rst_directive
8from .sphinxm_convert_doc_sphinx_helper import (
9 HTMLWriterWithCustomDirectives, _CustomSphinx,
10 MDWriterWithCustomDirectives, RSTWriterWithCustomDirectives,
11 LatexWriterWithCustomDirectives, DocTreeWriterWithCustomDirectives)
12from ..sphinxext import get_default_extensions
15class MockSphinxApp:
16 """
17 Mocks :epkg:`Sphinx` application.
18 In memory :epkg:`Sphinx` application.
19 """
21 def __init__(self, writer, app, confoverrides, new_extensions=None):
22 """
23 @param writer see static method create
24 @param app see static method create
25 @param confoverrides default options
26 @param new_extensions additional extensions
27 """
28 from sphinx.registry import SphinxComponentRegistry
29 if confoverrides is None:
30 confoverrides = {}
31 self.app = app
32 self.env = app.env
33 self.new_options = {}
34 self.writer = writer
35 self.registry = SphinxComponentRegistry()
36 self.mapping = {"<class 'sphinx.ext.todo.todo_node'>": "todo",
37 "<class 'sphinx.ext.graphviz.graphviz'>": "graphviz",
38 "<class 'sphinx.ext.mathbase.math'>": "math",
39 "<class 'sphinx.ext.mathbase.displaymath'>": "displaymath",
40 "<class 'sphinx.ext.mathbase.eqref'>": "eqref"}
42 # delayed import to speed up import time
43 from sphinx.config import Config
45 self.mapping_connect = {}
46 with warnings.catch_warnings():
47 warnings.simplefilter(
48 "ignore", (DeprecationWarning, PendingDeprecationWarning))
49 try:
50 self.config = Config( # pylint: disable=E1121
51 None, None, confoverrides, None) # pylint: disable=E1121
52 except TypeError:
53 # Sphinx>=3.0.0
54 self.config = Config({}, confoverrides)
55 self.confdir = "."
56 self.doctreedir = "."
57 self.srcdir = "."
58 self.builder = writer.builder
59 self._new_extensions = new_extensions
60 if id(self.app) != id(self.writer.app):
61 raise RuntimeError( # pragma: no cover
62 "Different application in the writer is not allowed.")
64 @property
65 def extensions(self):
66 return self.app.extensions
68 def add_directive(self, name, cl, *args, **options):
69 """
70 See :epkg:`class Sphinx`.
71 """
72 # doc_directives.register_directive(name, cl)
73 self.mapping[str(cl)] = name
74 self.app.add_directive(name, cl, *args, **options)
76 def add_role(self, name, cl):
77 """
78 See :epkg:`class Sphinx`.
79 """
80 # doc_roles.register_canonical_role(name, cl)
81 self.mapping[str(cl)] = name
82 self.app.add_role(name, cl)
84 def add_builder(self, name, cl):
85 """
86 See :epkg:`class Sphinx`.
87 """
88 self.mapping[str(cl)] = name
89 self.app.add_builder(name, cl)
91 def add_mapping(self, name, cl):
92 """
93 See :epkg:`class Sphinx`.
94 """
95 self.mapping[str(cl)] = name
97 def add_config_value(self, name, default, rebuild, types=()):
98 """
99 See :epkg:`class Sphinx`.
100 """
101 if name in self.config.values:
102 # We do not add it a second time.
103 return
104 if rebuild in (False, True):
105 rebuild = 'env' if rebuild else ''
106 self.new_options[name] = (default, rebuild, types)
107 self.config.values[name] = (default, rebuild, types)
109 def get_default_values(self):
110 """
111 See :epkg:`class Sphinx`.
112 """
113 return {k: v[0] for k, v in self.new_options.items()}
115 def add_node(self, node, **kwds):
116 """
117 See :epkg:`class Sphinx`.
118 """
119 self.app.add_node(node, **kwds)
121 def finalize(self, doctree, external_docnames=None):
122 """
123 Finalizes the documentation after it was parsed.
125 @param doctree doctree (or pub.document), available after publication
126 @param external_docnames other docnames the doctree references
127 """
128 self.app.finalize(doctree, external_docnames=external_docnames)
130 def setup_extension(self, extname):
131 """
132 See :epkg:`class Sphinx`.
133 """
134 self.app.setup_extension(extname)
136 def emit(self, event, *args):
137 """
138 See :epkg:`class Sphinx`.
139 """
140 return self.app.events.emit(event, *args)
142 def emit_firstresult(self, event, *args):
143 """
144 See :epkg:`class Sphinx`.
145 """
146 return self.app.events.emit_firstresult(event, self, *args)
148 def add_autodocumenter(self, cls):
149 """
150 See :epkg:`class Sphinx`.
151 """
152 from sphinx.ext.autodoc.directive import AutodocDirective
153 self.registry.add_documenter(cls.objtype, cls)
154 self.add_directive('auto' + cls.objtype, AutodocDirective)
156 def connect(self, node, func):
157 """
158 See :epkg:`class Sphinx`.
159 """
160 self.mapping_connect[node] = func
161 self.app.connect(node, func)
163 def add_domain(self, domain):
164 """
165 See :epkg:`class Sphinx`.
166 """
167 if domain.name in self.app.domains:
168 # We do not register it a second time.
169 return
170 self.app.domains[domain.name] = domain
172 def require_sphinx(self, version):
173 # check the Sphinx version if requested
174 # delayed import to speed up import time
175 from sphinx import __display_version__ as sphinx__display_version__
176 from sphinx.application import VersionRequirementError
177 if version > sphinx__display_version__[:3]:
178 raise VersionRequirementError(version)
180 def add_event(self, name):
181 """
182 See :epkg:`class Sphinx`.
183 """
184 if name in self.app._events:
185 # We do not raise an exception if already present.
186 return
187 self.app._events[name] = ''
189 def add_env_collector(self, collector):
190 """
191 See :epkg:`class Sphinx`.
192 """
193 self.app.add_env_collector(collector)
195 def add_js_file(self, jsfile):
196 """
197 See :epkg:`class Sphinx`.
198 """
199 try:
200 # Sphinx >= 1.8
201 self.app.add_js_file(jsfile)
202 except AttributeError: # pragma: no cover
203 # Sphinx < 1.8
204 self.app.add_javascript(jsfile)
206 def add_css_file(self, css):
207 """
208 See :epkg:`class Sphinx`.
209 """
210 try:
211 # Sphinx >= 1.8
212 self.app.add_css_file(css)
213 except AttributeError: # pragma: no cover
214 # Sphinx < 1.8
215 self.app.add_stylesheet(css)
217 def add_source_parser(self, ext, parser, exc=False):
218 """
219 Registers a parser for a specific file extension.
221 @param ext file extension
222 @param parser parser
223 @param exc raises an exception if already done
225 Example:
227 ::
229 app.add_source_parser(self, ext, parser)
230 """
231 # delayed import to speed up import time
232 from sphinx.errors import ExtensionError
233 with warnings.catch_warnings():
234 warnings.simplefilter("ignore", ImportWarning)
236 try:
237 self.app.add_source_parser(ext, parser)
238 except TypeError: # pragma: no cover
239 # Sphinx==3.0.0
240 self.app.add_source_parser(parser)
241 except ExtensionError as e: # pragma: no cover
242 if exc:
243 raise
244 logger = logging.getLogger("MockSphinxApp")
245 logger.warning('[MockSphinxApp] %s', e)
247 def disconnect_env_collector(self, clname):
248 """
249 Disconnects a collector given its class name.
251 @param cl name
252 @return found collector
253 """
254 self.app.disconnect_env_collector(clname)
256 @staticmethod
257 def create(writer="html", directives=None, confoverrides=None,
258 new_extensions=None, destination_path=None, fLOG=None):
259 """
260 Creates a @see cl MockSphinxApp for :epkg:`Sphinx`.
262 @param writer ``'sphinx'`` is the only allowed value
263 @param directives new directives to add (see below)
264 @param confoverrides initial options
265 @param new_extensions additional extensions to setup
266 @param destination_path some extension requires it
267 @param fLOG logging function
268 @return mockapp, writer, list of added nodes
270 *directives* is None or a list of 2 or 5-uple:
272 * a directive name (mandatory)
273 * a directive class: see `Sphinx Directive
274 <https://www.sphinx-doc.org/en/master/development/tutorials/helloworld.html>`_,
275 see also @see cl RunPythonDirective as an example (mandatory)
276 * a docutils node: see @see cl runpython_node as an example
277 * two functions: see @see fn visit_runpython_node,
278 @see fn depart_runpython_node as an example
279 """
280 logger = logging.getLogger('gdot')
281 if not logger.disabled:
282 logger.disabled = True
283 restore = True
284 else:
285 restore = False
287 with warnings.catch_warnings():
288 warnings.simplefilter(
289 "ignore", (DeprecationWarning, PendingDeprecationWarning))
290 if confoverrides is None:
291 confoverrides = {}
292 if "extensions" not in confoverrides:
293 confoverrides["extensions"] = get_default_extensions()
295 if writer in ("sphinx", "custom", "HTMLWriterWithCustomDirectives", "html"):
296 app = _CustomSphinx(srcdir=None, confdir=None, outdir=destination_path,
297 doctreedir=None, buildername='memoryhtml',
298 confoverrides=confoverrides, new_extensions=new_extensions)
299 writer = HTMLWriterWithCustomDirectives(
300 builder=app.builder, app=app)
301 mockapp = MockSphinxApp(writer, writer.app, confoverrides=confoverrides,
302 new_extensions=new_extensions)
303 elif writer == "rst":
304 app = _CustomSphinx(srcdir=None, confdir=None, outdir=destination_path,
305 doctreedir=None,
306 buildername='memoryrst', confoverrides=confoverrides,
307 new_extensions=new_extensions)
308 writer = RSTWriterWithCustomDirectives(
309 builder=app.builder, app=app)
310 mockapp = MockSphinxApp(writer, writer.app, confoverrides=confoverrides,
311 new_extensions=new_extensions)
312 elif writer == "md":
313 app = _CustomSphinx(srcdir=None, confdir=None, outdir=destination_path,
314 doctreedir=None,
315 buildername='memorymd', confoverrides=confoverrides,
316 new_extensions=new_extensions)
317 writer = MDWriterWithCustomDirectives(
318 builder=app.builder, app=app)
319 mockapp = MockSphinxApp(writer, writer.app, confoverrides=confoverrides,
320 new_extensions=new_extensions)
321 elif writer == "elatex":
322 app = _CustomSphinx(srcdir=None, confdir=None, outdir=None, doctreedir=None,
323 buildername='memorylatex', confoverrides=confoverrides,
324 new_extensions=new_extensions)
325 writer = LatexWriterWithCustomDirectives(
326 builder=app.builder, app=app)
327 mockapp = MockSphinxApp(writer, writer.app, confoverrides=confoverrides,
328 new_extensions=new_extensions)
329 elif writer == "doctree":
330 app = _CustomSphinx(srcdir=None, confdir=None, outdir=destination_path,
331 doctreedir=None,
332 buildername='memorydoctree', confoverrides=confoverrides,
333 new_extensions=new_extensions)
334 writer = DocTreeWriterWithCustomDirectives(
335 builder=app.builder, app=app)
336 mockapp = MockSphinxApp(writer, writer.app, confoverrides=confoverrides,
337 new_extensions=new_extensions)
338 elif isinstance(writer, tuple):
339 # We expect ("builder_name", builder_class)
340 app = _CustomSphinx(srcdir=None, confdir=None, outdir=destination_path,
341 doctreedir=None,
342 buildername=writer, confoverrides=confoverrides,
343 new_extensions=new_extensions)
344 if not hasattr(writer[1], "_writer_class"):
345 raise AttributeError( # pragma: no cover
346 "Class '{0}' does not have any attribute '_writer_class'."
347 "".format(writer[1]))
348 writer = writer[1]._writer_class( # pylint: disable=E1101
349 builder=app.builder, app=app) # pylint: disable=E1101
350 mockapp = MockSphinxApp(writer, app, confoverrides=confoverrides,
351 new_extensions=new_extensions)
352 else:
353 raise ValueError(
354 f"Writer must be 'html', 'rst', 'md', 'elatex', not '{writer}'.")
356 if restore:
357 logger.disabled = False
359 # titles
360 title_names = []
361 title_names.append("todoext_node")
362 title_names.append("todo_node")
363 title_names.append("mathdef_node")
364 title_names.append("blocref_node")
365 title_names.append("faqref_node")
366 title_names.append("nbref_node")
367 title_names.append("exref_node")
369 if directives is not None:
370 for tu in directives:
371 if len(tu) < 2:
372 raise ValueError(
373 "directives is a list of tuple with at least two elements, check the documentation")
374 if len(tu) == 5:
375 name, cl, node, f1, f2 = tu
376 mockapp.add_node(node, html=(f1, f2))
377 # not necessary
378 # nodes._add_node_class_names([node.__name__])
379 writer.connect_directive_node(node.__name__, f1, f2)
380 elif len(tu) != 2:
381 raise ValueError(
382 "directives is a list of tuple with 2 or 5 elements, check the documentation")
383 name, cl = tu[:2]
384 mockapp.add_directive(name, cl)
386 if fLOG:
387 apps = [mockapp]
388 if hasattr(writer, "app"):
389 apps.append(writer.app)
390 for app in apps:
391 if hasattr(app, "_added_objects"):
392 fLOG("[MockSphinxApp] list of added objects")
393 for el in app._added_objects:
394 fLOG("[MockSphinxApp]", el)
395 if el[0] == "domain":
396 fLOG("[MockSphinxApp] NAME", el[1].name)
397 for ro in el[1].roles:
398 fLOG("[MockSphinxApp] ROLES", ro)
399 for ro in el[1].directives:
400 fLOG("[MockSphinxApp] DIREC", ro)
401 from docutils.parsers.rst.directives import _directives
402 for res in sorted(_directives):
403 fLOG("[MockSphinxApp] RST DIREC", res)
405 class bb:
406 def info(*args, line=0): # pylint: disable=E0211,E0213
407 fLOG("[MockSphinxApp] -- ", *args)
409 class aa:
410 def __init__(self):
411 self.reporter = bb()
412 self.current_line = 0
413 from docutils.parsers.rst.languages import en
414 for dir_check in ['py:function']:
415 res = rst_directive(dir_check, en, aa())
417 return mockapp, writer, title_names