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