Coverage for pyquickhelper/helpgen/sphinxm_convert_doc_sphinx_helper.py: 90%
762 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 os
6import sys
7from collections import deque
8import warnings
9import pickle
10import platform
11from html import escape as htmlescape
12from io import StringIO
13from docutils.parsers.rst import roles
14from docutils.languages import en as docutils_en
15from docutils import nodes
16from docutils.utils import Reporter
17from sphinx.application import Sphinx
18from sphinx.environment import BuildEnvironment
19from sphinx.errors import ExtensionError
20from sphinx.ext.extlinks import setup_link_roles
21from sphinx.transforms import SphinxTransformer
22from sphinx.writers.html import HTMLWriter
23from sphinx.util.build_phase import BuildPhase
24from sphinx.util.logging import prefixed_warnings
25from sphinx.project import Project
26from sphinx.errors import ApplicationError
27from sphinx.util.logging import getLogger
28from ..sphinxext.sphinx_doctree_builder import (
29 DocTreeBuilder, DocTreeWriter, DocTreeTranslator)
30from ..sphinxext.sphinx_md_builder import MdBuilder, MdWriter, MdTranslator
31from ..sphinxext.sphinx_latex_builder import (
32 EnhancedLaTeXBuilder, EnhancedLaTeXWriter, EnhancedLaTeXTranslator)
33from ..sphinxext.sphinx_rst_builder import RstBuilder, RstWriter, RstTranslator
34from ._single_file_html_builder import CustomSingleFileHTMLBuilder
37def _get_LaTeXTranslator():
38 try:
39 from sphinx.writers.latex import LaTeXTranslator
40 except ImportError: # pragma: no cover
41 # Since sphinx 1.7.3 (circular reference).
42 import sphinx.builders.latex.transforms
43 from sphinx.writers.latex import LaTeXTranslator
44 return LaTeXTranslator
47try:
48 from sphinx.util.docutils import is_html5_writer_available
49except ImportError:
50 def is_html5_writer_available():
51 return True
53if is_html5_writer_available():
54 from sphinx.writers.html5 import HTML5Translator as HTMLTranslator
55else:
56 from sphinx.writers.html import HTMLTranslator # pragma: no cover
59def update_docutils_languages(values=None):
60 """
61 Updates ``docutils/languages/en.py`` with missing labels.
62 It Does it for languages *en*.
64 @param values consider values in this dictionaries first
65 """
66 if values is None:
67 values = dict()
68 lab = docutils_en.labels
69 if 'versionmodified' not in lab:
70 lab['versionmodified'] = values.get(
71 'versionmodified', 'modified version')
72 if 'desc' not in lab:
73 lab['desc'] = values.get('desc', 'description')
76class _AdditionalVisitDepart:
77 """
78 Additional visitors and departors.
79 """
81 def __init__(self, output_format):
82 self.output_format = output_format
84 def is_html(self):
85 """
86 Tells if the translator is :epkg:`html` format.
87 """
88 return self.base_class is HTMLTranslator
90 def is_rst(self):
91 """
92 Tells if the translator is :epkg:`rst` format.
93 """
94 return self.base_class is RstTranslator
96 def is_latex(self):
97 """
98 Tells if the translator is :epkg:`latex` format.
99 """
100 return self.base_class is _get_LaTeXTranslator()
102 def is_md(self):
103 """
104 Tells if the translator is :epkg:`markdown` format.
105 """
106 return self.base_class is _get_LaTeXTranslator()
108 def is_doctree(self):
109 """
110 Tells if the translator is doctree format.
111 """
112 return self.base_class is _get_LaTeXTranslator()
114 def add_secnumber(self, node):
115 """
116 Overwrites this method to catch errors due when
117 it is a single document being processed.
118 """
119 if node.get('secnumber'):
120 self.base_class.add_secnumber(self, node)
121 elif len(node.parent['ids']) > 0:
122 self.base_class.add_secnumber(self, node)
123 else:
124 n = len(self.builder.secnumbers)
125 node.parent['ids'].append("custom_label_%d" % n)
126 self.base_class.add_secnumber(self, node)
128 def eval_expr(self, expr):
129 rst = self.output_format == 'rst'
130 latex = self.output_format in ('latex', 'elatex')
131 texinfo = [('index', 'A_AdditionalVisitDepart', 'B_AdditionalVisitDepart', # pylint: disable=W0612
132 'C_AdditionalVisitDepart', 'D_AdditionalVisitDepart',
133 'E_AdditionalVisitDepart', 'Miscellaneous')]
134 html = self.output_format == 'html'
135 md = self.output_format == 'md'
136 doctree = self.output_format in ('doctree', 'doctree.txt')
137 if not (rst or html or latex or md or doctree):
138 raise ValueError( # pragma: no cover
139 f"Unknown output format '{self.output_format}'.")
140 try:
141 ev = eval(expr)
142 except Exception: # pragma: no cover
143 raise ValueError(
144 f"Unable to interpret expression '{expr}'")
145 return ev
147 def visit_only(self, node):
148 ev = self.eval_expr(node.attributes['expr'])
149 if ev:
150 pass
151 else:
152 raise nodes.SkipNode
154 def depart_only(self, node):
155 ev = self.eval_expr(node.attributes['expr'])
156 if ev:
157 pass
158 else:
159 # The program should not necessarily be here.
160 pass
162 def visit_viewcode_anchor(self, node):
163 # Removed in sphinx 3.5
164 pass
166 def depart_viewcode_anchor(self, node):
167 # Removed in sphinx 3.5
168 pass
170 def unknown_visit(self, node): # pragma: no cover
171 raise NotImplementedError(
172 "[_AdditionalVisitDepart] Unknown node: '{0}' in '{1}'".format(
173 node.__class__.__name__, self.__class__.__name__))
176class HTMLTranslatorWithCustomDirectives(_AdditionalVisitDepart, HTMLTranslator):
177 """
178 See @see cl HTMLWriterWithCustomDirectives.
179 """
181 def __init__(self, document, builder, *args, **kwds):
182 HTMLTranslator.__init__(self, document, builder, *args, **kwds)
183 _AdditionalVisitDepart.__init__(self, 'html')
184 nodes_list = getattr(builder, '_function_node', None)
185 if nodes_list is not None:
186 for name, f1, f2 in nodes_list:
187 setattr(self.__class__, "visit_" + name, f1)
188 setattr(self.__class__, "depart_" + name, f2)
189 self.base_class = HTMLTranslator
191 def visit_field(self, node):
192 if not hasattr(self, '_fieldlist_row_index'):
193 # needed when a docstring starts with :param:
194 self._fieldlist_row_index = 0
195 return HTMLTranslator.visit_field(self, node)
197 def visit_pending_xref(self, node):
198 self.visit_Text(node)
199 raise nodes.SkipNode
201 def unknown_visit(self, node): # pragma: no cover
202 raise NotImplementedError("[HTMLTranslatorWithCustomDirectives] Unknown node: '{0}' in '{1}'".format(
203 node.__class__.__name__, self.__class__.__name__))
206class RSTTranslatorWithCustomDirectives(_AdditionalVisitDepart, RstTranslator):
207 """
208 See @see cl HTMLWriterWithCustomDirectives.
209 """
211 def __init__(self, document, builder, *args, **kwds):
212 """
213 constructor
214 """
215 RstTranslator.__init__(self, document, builder, *args, **kwds)
216 _AdditionalVisitDepart.__init__(self, 'rst')
217 for name, f1, f2 in builder._function_node:
218 setattr(self.__class__, "visit_" + name, f1)
219 setattr(self.__class__, "depart_" + name, f2)
220 self.base_class = RstTranslator
223class MDTranslatorWithCustomDirectives(_AdditionalVisitDepart, MdTranslator):
224 """
225 See @see cl HTMLWriterWithCustomDirectives.
226 """
228 def __init__(self, document, builder, *args, **kwds):
229 """
230 constructor
231 """
232 MdTranslator.__init__(self, document, builder, *args, **kwds)
233 _AdditionalVisitDepart.__init__(self, 'md')
234 for name, f1, f2 in builder._function_node:
235 setattr(self.__class__, "visit_" + name, f1)
236 setattr(self.__class__, "depart_" + name, f2)
237 self.base_class = MdTranslator
240class DocTreeTranslatorWithCustomDirectives(DocTreeTranslator):
241 """
242 See @see cl HTMLWriterWithCustomDirectives.
243 """
245 def __init__(self, document, builder, *args, **kwds):
246 """
247 constructor
248 """
249 DocTreeTranslator.__init__(self, document, builder, *args, **kwds)
250 self.base_class = DocTreeTranslator
253class LatexTranslatorWithCustomDirectives(_AdditionalVisitDepart, EnhancedLaTeXTranslator):
254 """
255 See @see cl LatexWriterWithCustomDirectives.
256 """
258 def __init__(self, document, builder, *args, **kwds):
259 """
260 constructor
261 """
262 if not hasattr(builder, "config"):
263 builder, document = document, builder
264 if not hasattr(builder, "config"):
265 raise TypeError( # pragma: no cover
266 f"Builder has no config: {type(builder)} - {type(document)}")
267 EnhancedLaTeXTranslator.__init__(
268 self, document, builder, *args, **kwds)
269 _AdditionalVisitDepart.__init__(self, 'md')
270 for name, f1, f2 in builder._function_node:
271 setattr(self.__class__, "visit_" + name, f1)
272 setattr(self.__class__, "depart_" + name, f2)
273 self.base_class = EnhancedLaTeXTranslator
276class _WriterWithCustomDirectives:
277 """
278 Common class to @see cl HTMLWriterWithCustomDirectives and @see cl RSTWriterWithCustomDirectives.
279 """
281 def _init(self, base_class, translator_class, app=None):
282 """
283 @param base_class base class
284 @param app Sphinx application
285 """
286 if app is None:
287 self.app = _CustomSphinx(srcdir=None, confdir=None, outdir=None, doctreedir=None,
288 buildername='memoryhtml')
289 else:
290 self.app = app
291 builder = self.app.builder
292 builder.fignumbers = {}
293 base_class.__init__(self, builder)
294 self.translator_class = translator_class
295 self.builder.secnumbers = {}
296 self.builder._function_node = []
297 self.builder.current_docname = None
298 self.base_class = base_class
300 def connect_directive_node(self, name, f_visit, f_depart):
301 """
302 Adds custom node to the translator.
304 @param name name of the directive
305 @param f_visit visit function
306 @param f_depart depart function
307 """
308 if self.builder.format != "doctree":
309 self.builder._function_node.append((name, f_visit, f_depart))
311 def add_configuration_options(self, new_options):
312 """
313 Add new options.
315 @param new_options new options
316 """
317 for k, v in new_options.items():
318 self.builder.config.values[k] = v
320 def write(self, document, destination):
321 """
322 Processes a document into its final form.
323 Translates `document` (a Docutils document tree) into the Writer's
324 native format, and write it out to its `destination` (a
325 `docutils.io.Output` subclass object).
327 Normally not overridden or extended in subclasses.
328 """
329 self.base_class.write(self, document, destination)
332class HTMLWriterWithCustomDirectives(_WriterWithCustomDirectives, HTMLWriter):
333 """
334 This :epkg:`docutils` writer extends the HTML writer with
335 custom directives implemented in this module,
336 @see cl RunPythonDirective, @see cl BlogPostDirective.
338 See `Write your own ReStructuredText-Writer <http://www.arnebrodowski.de/blog/write-your-own-restructuredtext-writer.html>`_.
340 This class needs to tell :epkg:`docutils` to call the added function
341 when directives *runpython* or *blogpost* are met.
342 """
344 def __init__(self, builder=None, app=None): # pylint: disable=W0231
345 """
346 @param builder builder
347 @param app Sphinx application
348 """
349 _WriterWithCustomDirectives._init(
350 self, HTMLWriter, HTMLTranslatorWithCustomDirectives, app)
352 def translate(self):
353 self.visitor = visitor = self.translator_class(
354 self.document, self.builder)
355 self.document.walkabout(visitor)
356 self.output = visitor.astext()
357 for attr in ('head_prefix', 'stylesheet', 'head', 'body_prefix',
358 'body_pre_docinfo', 'docinfo', 'body', 'fragment',
359 'body_suffix', 'meta', 'title', 'subtitle', 'header',
360 'footer', 'html_prolog', 'html_head', 'html_title',
361 'html_subtitle', 'html_body', ):
362 setattr(self, attr, getattr(visitor, attr, None))
363 self.clean_meta = ''.join(visitor.meta[2:])
366class RSTWriterWithCustomDirectives(_WriterWithCustomDirectives, RstWriter):
367 """
368 This :epkg:`docutils` writer extends the :epkg:`RST` writer with
369 custom directives implemented in this module.
370 """
372 def __init__(self, builder=None, app=None): # pylint: disable=W0231
373 """
374 @param builder builder
375 @param app Sphinx application
376 """
377 _WriterWithCustomDirectives._init(
378 self, RstWriter, RSTTranslatorWithCustomDirectives, app)
380 def translate(self):
381 visitor = self.translator_class(self.document, self.builder)
382 self.document.walkabout(visitor)
383 self.output = visitor.body
386class MDWriterWithCustomDirectives(_WriterWithCustomDirectives, MdWriter):
387 """
388 This :epkg:`docutils` writer extends the :epkg:`MD` writer with
389 custom directives implemented in this module.
390 """
392 def __init__(self, builder=None, app=None): # pylint: disable=W0231
393 """
394 @param builder builder
395 @param app Sphinx application
396 """
397 _WriterWithCustomDirectives._init(
398 self, MdWriter, MDTranslatorWithCustomDirectives, app)
400 def translate(self):
401 visitor = self.translator_class(self.document, self.builder)
402 self.document.walkabout(visitor)
403 self.output = visitor.body
406class DocTreeWriterWithCustomDirectives(_WriterWithCustomDirectives, DocTreeWriter):
407 """
408 This :epkg:`docutils` writer creates a doctree writer with
409 custom directives implemented in this module.
410 """
412 def __init__(self, builder=None, app=None): # pylint: disable=W0231
413 """
414 @param builder builder
415 @param app Sphinx application
416 """
417 _WriterWithCustomDirectives._init(
418 self, DocTreeWriter, DocTreeTranslatorWithCustomDirectives, app)
420 def translate(self):
421 visitor = self.translator_class(self.document, self.builder)
422 self.document.walkabout(visitor)
423 self.output = visitor.body
426class LatexWriterWithCustomDirectives(_WriterWithCustomDirectives, EnhancedLaTeXWriter):
427 """
428 This :epkg:`docutils` writer extends the :epkg:`Latex` writer with
429 custom directives implemented in this module.
430 """
432 def __init__(self, builder=None, app=None): # pylint: disable=W0231
433 """
434 @param builder builder
435 @param app Sphinx application
436 """
437 _WriterWithCustomDirectives._init(
438 self, EnhancedLaTeXWriter, LatexTranslatorWithCustomDirectives, app)
439 if not hasattr(self.builder, "config"):
440 raise TypeError( # pragma: no cover
441 f"Builder has no config: {type(self.builder)}")
443 def translate(self):
444 if not hasattr(self.builder, "config"):
445 raise TypeError( # pragma: no cover
446 f"Builder has no config: {type(self.builder)}")
447 # The instruction
448 # visitor = self.builder.create_translator(self.document, self.builder)
449 # automatically adds methods visit_ and depart_ for translator
450 # based on the list of registered extensions. Might be worth using it.
451 theme = self.builder.themes.get('manual')
452 if theme is None:
453 raise RuntimeError( # pragma: no cover
454 "theme cannot be None.")
455 visitor = self.translator_class(
456 self.document, self.builder, theme=theme)
457 self.document.walkabout(visitor)
458 self.output = visitor.body
461class _MemoryBuilder:
462 """
463 Builds :epkg:`HTML` output in memory.
464 The API is defined by the page
465 :epkg:`builderapi`.
466 """
468 def _init(self, base_class, app, env=None):
469 """
470 Constructs the builder.
471 Most of the parameter are static members of the class and cannot
472 be overwritten (yet).
474 :param base_class: base builder class
475 :param app: :epkg:`Sphinx application`
476 :param env: Environment
477 """
478 if "IMPOSSIBLE:TOFIND" in app.srcdir:
479 import sphinx.util.osutil
480 from .conf_path_tools import custom_ensuredir
481 sphinx.util.osutil.ensuredir = custom_ensuredir
482 sphinx.builders.ensuredir = custom_ensuredir
484 try:
485 base_class.__init__(self, app=app, env=env)
486 except TypeError:
487 # older version of sphinx
488 base_class.__init__(self, app=app)
489 self.built_pages = {}
490 self.base_class = base_class
492 def iter_pages(self):
493 """
494 Enumerate created pages.
496 @return iterator on tuple(name, content)
497 """
498 for k, v in self.built_pages.items():
499 yield k, v.getvalue()
501 def create_translator(self, *args):
502 """
503 Returns an instance of translator.
504 This method returns an instance of ``default_translator_class`` by default.
505 Users can replace the translator class with ``app.set_translator()`` API.
506 """
507 translator_class = self.translator_class
508 return translator_class(*args)
510 def _write_serial(self, docnames):
511 """
512 Overwrites *_write_serial* to avoid writing on disk.
513 """
514 from sphinx.util.logging import pending_warnings
515 try:
516 from sphinx.util.display import status_iterator
517 except ImportError:
518 from sphinx.util import status_iterator
519 with pending_warnings():
520 for docname in status_iterator(docnames, 'writing output... ', "darkgreen",
521 len(docnames), self.app.verbosity):
522 doctree = self.env.get_and_resolve_doctree(docname, self)
523 self.write_doc_serialized(docname, doctree)
524 self.write_doc(docname, doctree)
526 def _write_parallel(self, docnames, nproc):
527 """
528 Not supported.
529 """
530 raise NotImplementedError(
531 "Use parallel=0 when creating the sphinx application.")
533 def assemble_doctree(self, *args, **kwargs):
534 """
535 Overwrites *assemble_doctree* to control the doctree.
536 """
537 from sphinx.util.nodes import inline_all_toctrees
538 from sphinx.util.console import darkgreen
539 master = self.config.master_doc
540 if hasattr(self, "doctree_"):
541 tree = self.doctree_
542 else:
543 raise AttributeError( # pragma: no cover
544 "Attribute 'doctree_' is not present. Call method finalize().")
545 tree = inline_all_toctrees(
546 self, set(), master, tree, darkgreen, [master])
547 tree['docname'] = master
548 self.env.resolve_references(tree, master, self)
549 self.fix_refuris(tree)
550 return tree
552 def fix_refuris(self, tree):
553 """
554 Overwrites *fix_refuris* to control the reference names.
555 """
556 fname = "__" + self.config.master_doc + "__"
557 for refnode in tree.traverse(nodes.reference):
558 if 'refuri' not in refnode:
559 continue
560 refuri = refnode['refuri']
561 hashindex = refuri.find('#')
562 if hashindex < 0:
563 continue
564 hashindex = refuri.find('#', hashindex + 1)
565 if hashindex >= 0:
566 refnode['refuri'] = fname + refuri[hashindex:]
568 def get_target_uri(self, docname, typ=None):
569 """
570 Overwrites *get_target_uri* to control the page name.
571 """
572 if docname in self.env.all_docs:
573 # all references are on the same page...
574 return self.config.master_doc + '#document-' + docname
575 elif docname in ("genindex", "search"):
576 return self.config.master_doc + '-#' + docname
577 else:
578 docs = ", ".join( # pragma: no cover
579 sorted(f"'{_}'" for _ in self.env.all_docs))
580 raise ValueError( # pragma: no cover
581 f"docname='{docname}' should be in 'self.env.all_docs' which contains:\n{docs}")
583 def get_outfilename(self, pagename):
584 """
585 Overwrites *get_target_uri* to control file names.
586 """
587 return f"{self.outdir}/{pagename}.m.html".replace("\\", "/")
589 def handle_page(self, pagename, addctx, templatename='page.html',
590 outfilename=None, event_arg=None):
591 """
592 Overrides *handle_page* to write into stream instead of files.
593 """
594 from sphinx.util.osutil import relative_uri
595 ctx = self.globalcontext.copy()
596 if hasattr(self, "warning"):
597 ctx['warn'] = self.warning
598 elif hasattr(self, "warn"):
599 ctx['warn'] = self.warn
600 # current_page_name is backwards compatibility
601 ctx['pagename'] = ctx['current_page_name'] = pagename
602 ctx['encoding'] = self.config.html_output_encoding
603 default_baseuri = self.get_target_uri(pagename)
604 # in the singlehtml builder, default_baseuri still contains an #anchor
605 # part, which relative_uri doesn't really like...
606 default_baseuri = default_baseuri.rsplit('#', 1)[0]
608 def pathto(otheruri, resource=False, baseuri=default_baseuri):
609 if resource and '://' in otheruri:
610 # allow non-local resources given by scheme
611 return otheruri
612 elif not resource:
613 otheruri = self.get_target_uri(otheruri)
614 uri = relative_uri(baseuri, otheruri) or '#'
615 if uri == '#' and not self.allow_sharp_as_current_path:
616 uri = baseuri
617 return uri
618 ctx['pathto'] = pathto
620 def css_tag(css):
621 attrs = []
622 for key in sorted(css.attributes):
623 value = css.attributes[key]
624 if value is not None:
625 attrs.append('%s="%s"' % (key, htmlescape( # pylint: disable=W1505
626 value, True))) # pylint: disable=W1505
627 attrs.append(f'href="{pathto(css.filename, resource=True)}"')
628 return f"<link {' '.join(attrs)} />"
629 ctx['css_tag'] = css_tag
631 def hasdoc(name):
632 if name in self.env.all_docs:
633 return True
634 elif name == 'search' and self.search:
635 return True
636 elif name == 'genindex' and self.get_builder_config('use_index', 'html'):
637 return True
638 return False
639 ctx['hasdoc'] = hasdoc
641 ctx['toctree'] = lambda **kw: self._get_local_toctree(pagename, **kw)
642 self.add_sidebars(pagename, ctx)
643 ctx.update(addctx)
645 self.update_page_context(pagename, templatename, ctx, event_arg)
646 newtmpl = self.app.emit_firstresult('html-page-context', pagename,
647 templatename, ctx, event_arg)
648 if newtmpl:
649 templatename = newtmpl
651 try:
652 output = self.templates.render(templatename, ctx)
653 except UnicodeError: # pragma: no cover
654 logger = getLogger("MockSphinxApp")
655 logger.warning("[_CustomSphinx] A unicode error occurred when rendering the page %s. "
656 "Please make sure all config values that contain "
657 "non-ASCII content are Unicode strings.", pagename)
658 return
660 if not outfilename:
661 outfilename = self.get_outfilename(pagename)
662 # outfilename's path is in general different from self.outdir
663 # ensuredir(path.dirname(outfilename))
664 if outfilename not in self.built_pages:
665 self.built_pages[outfilename] = StringIO()
666 self.built_pages[outfilename].write(output)
669class MemoryHTMLBuilder(_MemoryBuilder, CustomSingleFileHTMLBuilder):
670 """
671 Builds :epkg:`HTML` output in memory.
672 The API is defined by the page
673 :epkg:`builderapi`.
674 """
675 name = 'memoryhtml'
676 format = 'html'
677 out_suffix = None # ".memory.html"
678 supported_image_types = ['application/pdf', 'image/png', 'image/jpeg']
679 default_translator_class = HTMLTranslatorWithCustomDirectives
680 translator_class = HTMLTranslatorWithCustomDirectives
681 _writer_class = HTMLWriterWithCustomDirectives
682 supported_remote_images = True
683 supported_data_uri_images = True
684 html_scaled_image_link = True
686 def __init__(self, app, env=None): # pylint: disable=W0231
687 """
688 Construct the builder.
689 Most of the parameter are static members of the class and cannot
690 be overwritten (yet).
692 :param app: :epkg:`Sphinx application`
693 """
694 _MemoryBuilder._init(self, CustomSingleFileHTMLBuilder, app, env=env)
697class MemoryRSTBuilder(_MemoryBuilder, RstBuilder):
699 """
700 Builds :epkg:`RST` output in memory.
701 The API is defined by the page
702 :epkg:`builderapi`.
703 The writer simplifies the :epkg:`RST` syntax by replacing
704 custom roles into true :epkg:`RST` syntax.
705 """
707 name = 'memoryrst'
708 format = 'rst'
709 out_suffix = None # ".memory.rst"
710 supported_image_types = ['application/pdf', 'image/png', 'image/jpeg']
711 default_translator_class = RSTTranslatorWithCustomDirectives
712 translator_class = RSTTranslatorWithCustomDirectives
713 _writer_class = RSTWriterWithCustomDirectives
714 supported_remote_images = True
715 supported_data_uri_images = True
716 html_scaled_image_link = True
718 def __init__(self, app, env=None): # pylint: disable=W0231
719 """
720 Construct the builder.
721 Most of the parameter are static members of the class and cannot
722 be overwritten (yet).
724 :param app: :epkg:`Sphinx application`
725 """
726 _MemoryBuilder._init(self, RstBuilder, app, env=env)
728 def handle_page(self, pagename, addctx, templatename=None,
729 outfilename=None, event_arg=None):
730 """
731 Override *handle_page* to write into stream instead of files.
732 """
733 if templatename is not None:
734 raise NotImplementedError(
735 "templatename must be None.") # pragma: no cover
736 if not outfilename:
737 outfilename = self.get_outfilename(pagename)
738 if outfilename not in self.built_pages:
739 self.built_pages[outfilename] = StringIO()
740 self.built_pages[outfilename].write(self.writer.output)
743class MemoryMDBuilder(_MemoryBuilder, MdBuilder):
744 """
745 Builds :epkg:`MD` output in memory.
746 The API is defined by the page
747 :epkg:`builderapi`.
748 """
749 name = 'memorymd'
750 format = 'md'
751 out_suffix = None # ".memory.rst"
752 supported_image_types = ['application/pdf', 'image/png', 'image/jpeg']
753 default_translator_class = MDTranslatorWithCustomDirectives
754 translator_class = MDTranslatorWithCustomDirectives
755 _writer_class = MDWriterWithCustomDirectives
756 supported_remote_images = True
757 supported_data_uri_images = True
758 html_scaled_image_link = True
760 def __init__(self, app, env=None): # pylint: disable=W0231
761 """
762 Construct the builder.
763 Most of the parameter are static members of the class and cannot
764 be overwritten (yet).
766 :param app: :epkg:`Sphinx application`
767 """
768 _MemoryBuilder._init(self, MdBuilder, app, env=env)
770 def handle_page(self, pagename, addctx, templatename=None,
771 outfilename=None, event_arg=None):
772 """
773 Override *handle_page* to write into stream instead of files.
774 """
775 if templatename is not None:
776 raise NotImplementedError(
777 "templatename must be None.") # pragma: no cover
778 if not outfilename:
779 outfilename = self.get_outfilename(pagename)
780 if outfilename not in self.built_pages:
781 self.built_pages[outfilename] = StringIO()
782 self.built_pages[outfilename].write(self.writer.output)
785class MemoryDocTreeBuilder(_MemoryBuilder, DocTreeBuilder):
786 """
787 Builds doctree output in memory.
788 The API is defined by the page
789 :epkg:`builderapi`.
790 """
791 name = 'memorydoctree'
792 format = 'doctree'
793 out_suffix = None # ".memory.rst"
794 default_translator_class = DocTreeTranslatorWithCustomDirectives
795 translator_class = DocTreeTranslatorWithCustomDirectives
796 _writer_class = DocTreeWriterWithCustomDirectives
797 supported_remote_images = True
798 supported_data_uri_images = True
799 html_scaled_image_link = True
801 def __init__(self, app, env=None): # pylint: disable=W0231
802 """
803 Constructs the builder.
804 Most of the parameter are static members of the class and cannot
805 be overwritten (yet).
807 :param app: :epkg:`Sphinx application`
808 """
809 _MemoryBuilder._init(self, DocTreeBuilder, app, env=env)
811 def handle_page(self, pagename, addctx, templatename=None,
812 outfilename=None, event_arg=None):
813 """
814 Override *handle_page* to write into stream instead of files.
815 """
816 if templatename is not None:
817 raise NotImplementedError(
818 "templatename must be None.") # pragma: no cover
819 if not outfilename:
820 outfilename = self.get_outfilename(pagename)
821 if outfilename not in self.built_pages:
822 self.built_pages[outfilename] = StringIO()
823 self.built_pages[outfilename].write(self.writer.output)
826class MemoryLatexBuilder(_MemoryBuilder, EnhancedLaTeXBuilder):
827 """
828 Builds :epkg:`Latex` output in memory.
829 The API is defined by the page
830 :epkg:`builderapi`.
831 """
832 name = 'memorylatex'
833 format = 'tex'
834 out_suffix = None # ".memory.tex"
835 supported_image_types = ['image/png', 'image/jpeg', 'image/gif']
836 default_translator_class = LatexTranslatorWithCustomDirectives
837 translator_class = LatexTranslatorWithCustomDirectives
838 _writer_class = LatexWriterWithCustomDirectives
839 supported_remote_images = True
840 supported_data_uri_images = True
841 html_scaled_image_link = True
843 def __init__(self, app, env=None): # pylint: disable=W0231
844 """
845 Constructs the builder.
846 Most of the parameter are static members of the class and cannot
847 be overwritten (yet).
849 :param app: :epkg:`Sphinx application`
850 """
851 _MemoryBuilder._init(self, EnhancedLaTeXBuilder, app, env=env)
853 def write_stylesheet(self):
854 from sphinx.highlighting import PygmentsBridge
855 highlighter = PygmentsBridge('latex', self.config.pygments_style)
856 rows = []
857 rows.append('\\NeedsTeXFormat{LaTeX2e}[1995/12/01]\n')
858 rows.append('\\ProvidesPackage{sphinxhighlight}')
859 rows.append(
860 '[2016/05/29 stylesheet for highlighting with pygments]\n\n')
861 rows.append(highlighter.get_stylesheet())
862 self.built_pages['sphinxhighlight.sty'] = StringIO()
863 self.built_pages['sphinxhighlight.sty'].write("".join(rows))
865 class EnhancedStringIO(StringIO):
866 def write(self, content):
867 if isinstance(content, str):
868 StringIO.write(self, content)
869 else:
870 for line in content:
871 StringIO.write(self, line)
873 def _get_filename(self, targetname, encoding='utf-8', overwrite_if_changed=True):
874 if not isinstance(targetname, str):
875 raise TypeError( # pragma: no cover
876 f"targetname must be a string: {targetname}")
877 destination = MemoryLatexBuilder.EnhancedStringIO()
878 self.built_pages[targetname] = destination
879 return destination
882class _CustomBuildEnvironment(BuildEnvironment):
883 """
884 Overrides some functionalities of
885 `BuildEnvironment <https://www.sphinx-doc.org/en/master/extdev/envapi.html>`_.
886 """
888 def __init__(self, app):
889 """
890 """
891 BuildEnvironment.__init__(self, app)
892 self.doctree_ = {}
894 def get_doctree(self, docname):
895 """Read the doctree for a file from the pickle and return it."""
896 if hasattr(self, "doctree_") and docname in self.doctree_:
897 from sphinx.util.docutils import WarningStream
898 doctree = self.doctree_[docname]
899 doctree.settings.env = self
900 doctree.reporter = Reporter(self.doc2path(
901 docname), 2, 5, stream=WarningStream())
902 return doctree
904 if hasattr(self, "doctree_"):
905 available = list(sorted(self.doctree_))
906 if len(available) > 10:
907 available = available[10:]
908 raise KeyError(
909 "Unable to find entry '{}' (has doctree: {})\nFirst documents:\n{}"
910 "".format(
911 docname, hasattr(self, "doctree_"),
912 "\n".join(available)))
914 raise KeyError( # pragma: no cover
915 "Doctree empty or not found for '{}' (has doctree: {})"
916 "".format(
917 docname, hasattr(self, "doctree_")))
918 # return BuildEnvironment.get_doctree(self, docname)
920 def apply_post_transforms(self, doctree, docname):
921 """Apply all post-transforms."""
922 # set env.docname during applying post-transforms
923 self.temp_data['docname'] = docname
925 transformer = SphinxTransformer(doctree)
926 transformer.set_environment(self)
927 transformer.add_transforms(self.app.post_transforms)
928 transformer.apply_transforms()
929 self.temp_data.clear()
932class _CustomSphinx(Sphinx):
933 """
934 Custom :epkg:`Sphinx` application to avoid using disk.
935 """
937 def __init__(self, srcdir, confdir, outdir, doctreedir, buildername="memoryhtml", # pylint: disable=W0231
938 confoverrides=None, status=None, warning=None,
939 freshenv=False, warningiserror=False,
940 tags=None, verbosity=0, parallel=0, keep_going=False,
941 new_extensions=None):
942 '''
943 Same constructor as :epkg:`Sphinx application`.
944 Additional parameters:
946 @param new_extensions extensions to add to the application
948 Some insights about domains:
950 ::
952 {'cpp': sphinx.domains.cpp.CPPDomain,
953 'hpp': sphinx.domains.cpp.CPPDomain,
954 'h': sphinx.domains.cpp.CPPDomain,
955 'js': sphinx.domains.javascript.JavaScriptDomain,
956 'std': sphinx.domains.std.StandardDomain,
957 'py': sphinx.domains.python.PythonDomain,
958 'rst': sphinx.domains.rst.ReSTDomain,
959 'c': sphinx.domains.c.CDomain}
961 And builders:
963 ::
965 {'epub': ('epub', 'EpubBuilder'),
966 'singlehtml': ('html', 'SingleFileHTMLBuilder'),
967 'qthelp': ('qthelp', 'QtHelpBuilder'),
968 'epub3': ('epub3', 'Epub3Builder'),
969 'man': ('manpage', 'ManualPageBuilder'),
970 'dummy': ('dummy', 'DummyBuilder'),
971 'json': ('html', 'JSONHTMLBuilder'),
972 'html': ('html', 'StandaloneHTMLBuilder'),
973 'xml': ('xml', 'XMLBuilder'),
974 'texinfo': ('texinfo', 'TexinfoBuilder'),
975 'devhelp': ('devhelp', 'DevhelpBuilder'),
976 'web': ('html', 'PickleHTMLBuilder'),
977 'pickle': ('html', 'PickleHTMLBuilder'),
978 'htmlhelp': ('htmlhelp', 'HTMLHelpBuilder'),
979 'applehelp': ('applehelp', 'AppleHelpBuilder'),
980 'linkcheck': ('linkcheck', 'CheckExternalLinksBuilder'),
981 'dirhtml': ('html', 'DirectoryHTMLBuilder'),
982 'latex': ('latex', 'LaTeXBuilder'),
983 'elatex': ('latex', 'EnchancedLaTeXBuilder'),
984 'text': ('text', 'TextBuilder'),
985 'changes': ('changes', 'ChangesBuilder'),
986 'websupport': ('websupport', 'WebSupportBuilder'),
987 'gettext': ('gettext', 'MessageCatalogBuilder'),
988 'pseudoxml': ('xml', 'PseudoXMLBuilder')}
989 'rst': ('rst', 'RstBuilder')}
990 'md': ('md', 'MdBuilder'),
991 'doctree': ('doctree', 'DocTreeBuilder')}
992 '''
993 # own purpose (to monitor)
994 self._logger = getLogger("_CustomSphinx")
995 self._added_objects = []
996 self._added_collectors = []
998 # from sphinx.domains.cpp import CPPDomain
999 # from sphinx.domains.javascript import JavaScriptDomain
1000 # from sphinx.domains.python import PythonDomain
1001 # from sphinx.domains.std import StandardDomain
1002 # from sphinx.domains.rst import ReSTDomain
1003 # from sphinx.domains.c import CDomain
1005 from sphinx.registry import SphinxComponentRegistry
1006 self.phase = BuildPhase.INITIALIZATION
1007 self.verbosity = verbosity
1008 self.extensions = {}
1009 self.builder = None
1010 self.env = None
1011 self.project = None
1012 self.registry = SphinxComponentRegistry()
1013 self.post_transforms = []
1014 self.pdb = False
1016 if doctreedir is None:
1017 doctreedir = "IMPOSSIBLE:TOFIND"
1018 if srcdir is None:
1019 srcdir = "IMPOSSIBLE:TOFIND"
1020 update_docutils_languages()
1022 self.srcdir = os.path.abspath(srcdir)
1023 self.confdir = os.path.abspath(
1024 confdir) if confdir is not None else None
1025 self.outdir = os.path.abspath(outdir) if confdir is not None else None
1026 self.doctreedir = os.path.abspath(doctreedir)
1027 self.parallel = parallel
1029 if self.srcdir == self.outdir:
1030 raise ApplicationError('Source directory and destination ' # pragma: no cover
1031 'directory cannot be identical')
1033 if status is None:
1034 self._status = StringIO()
1035 self.quiet = True
1036 else:
1037 self._status = status
1038 self.quiet = False
1040 from sphinx.events import EventManager
1041 # logging.setup(self, self._status, self._warning)
1042 self.events = EventManager(self)
1044 # keep last few messages for traceback
1045 # This will be filled by sphinx.util.logging.LastMessagesWriter
1046 self.messagelog = deque(maxlen=10)
1048 # say hello to the world
1049 from sphinx import __display_version__
1050 self.info(f'Running Sphinx v{__display_version__}') # pragma: no cover
1052 # notice for parallel build on macOS and py38+
1053 if sys.version_info > (3, 8) and platform.system() == 'Darwin' and parallel > 1:
1054 self._logger.info( # pragma: no cover
1055 "For security reason, parallel mode is disabled on macOS and "
1056 "python3.8 and above. For more details, please read "
1057 "https://github.com/sphinx-doc/sphinx/issues/6803")
1059 # status code for command-line application
1060 self.statuscode = 0
1062 # delayed import to speed up time
1063 from sphinx.application import builtin_extensions
1064 from sphinx.config import CONFIG_FILENAME, Config, Tags
1066 # read config
1067 self.tags = Tags(tags)
1068 with warnings.catch_warnings():
1069 warnings.simplefilter(
1070 "ignore", (DeprecationWarning, PendingDeprecationWarning))
1071 if self.confdir is None:
1072 self.config = Config({}, confoverrides or {})
1073 else: # pragma: no cover
1074 try:
1075 self.config = Config.read(
1076 self.confdir, confoverrides or {}, self.tags)
1077 except AttributeError:
1078 try:
1079 self.config = Config( # pylint: disable=E1121
1080 confdir, confoverrides or {}, self.tags)
1081 except TypeError:
1082 try:
1083 self.config = Config(confdir, CONFIG_FILENAME, # pylint: disable=E1121
1084 confoverrides or {}, self.tags)
1085 except TypeError:
1086 # Sphinx==3.0.0
1087 self.config = Config({}, confoverrides or {})
1088 self.sphinx__display_version__ = __display_version__
1090 # create the environment
1091 self.config.pre_init_values()
1093 # set up translation infrastructure
1094 self._init_i18n()
1096 # check the Sphinx version if requested
1097 if (self.config.needs_sphinx and self.config.needs_sphinx >
1098 __display_version__): # pragma: no cover
1099 from sphinx.locale import _
1100 from sphinx.application import VersionRequirementError
1101 raise VersionRequirementError(
1102 _('This project needs at least Sphinx v%s and therefore cannot '
1103 'be built with this version.') % self.config.needs_sphinx)
1105 # set confdir to srcdir if -C given (!= no confdir); a few pieces
1106 # of code expect a confdir to be set
1107 if self.confdir is None:
1108 self.confdir = self.srcdir
1110 # load all built-in extension modules
1111 for extension in builtin_extensions:
1112 try:
1113 with warnings.catch_warnings():
1114 warnings.filterwarnings(
1115 "ignore", category=DeprecationWarning)
1116 self.setup_extension(extension)
1117 except Exception as e: # pragma: no cover
1118 if 'sphinx.builders.applehelp' not in str(e): # pragma: no cover
1119 mes = "Unable to run setup_extension '{0}'\nWHOLE LIST\n{1}".format(
1120 extension, "\n".join(builtin_extensions))
1121 raise ExtensionError(mes) from e
1123 # load all user-given extension modules
1124 for extension in self.config.extensions:
1125 self.setup_extension(extension)
1127 # /1 addition to the original code
1128 # additional extensions
1129 if new_extensions:
1130 for extension in new_extensions:
1131 if isinstance(extension, str):
1132 self.setup_extension(extension)
1133 else: # pragma: no cover
1134 # We assume it is a module.
1135 dirname = os.path.dirname(extension.__file__)
1136 sys.path.insert(0, dirname)
1137 self.setup_extension(extension.__name__)
1138 del sys.path[0]
1140 # add default HTML builders
1141 self.add_builder(MemoryHTMLBuilder)
1142 self.add_builder(MemoryRSTBuilder)
1143 self.add_builder(MemoryMDBuilder)
1144 self.add_builder(MemoryLatexBuilder)
1145 self.add_builder(MemoryDocTreeBuilder)
1147 if isinstance(buildername, tuple):
1148 if len(buildername) != 2:
1149 raise ValueError( # pragma: no cover
1150 "The builder can be custom but it must be specifed "
1151 "as a 2-uple=(builder_name, builder_class).")
1152 self.add_builder(buildername[1])
1153 buildername = buildername[0]
1155 # /1 end of addition
1157 # preload builder module (before init config values)
1158 self.preload_builder(buildername)
1160 # the config file itself can be an extension
1161 if self.config.setup:
1162 prefix = f"while setting up extension {'conf.py'}:"
1163 if prefixed_warnings is not None:
1164 with prefixed_warnings(prefix):
1165 if callable(self.config.setup):
1166 self.config.setup(self)
1167 else: # pragma: no cover
1168 from sphinx.locale import _
1169 from sphinx.application import ConfigError
1170 raise ConfigError(
1171 _("'setup' as currently defined in conf.py isn't a Python callable. "
1172 "Please modify its definition to make it a callable function. This is "
1173 "needed for conf.py to behave as a Sphinx extension.")
1174 )
1175 elif callable(self.config.setup):
1176 self.config.setup(self)
1178 # now that we know all config values, collect them from conf.py
1179 noallowed = []
1180 rem = []
1181 for k in confoverrides:
1182 if k in {'initial_header_level', 'doctitle_xform', 'input_encoding',
1183 'outdir', 'warnings_log', 'extensions'}:
1184 continue
1185 if k == 'override_image_directive':
1186 self.config.images_config["override_image_directive"] = True
1187 rem.append(k)
1188 continue
1189 if k not in self.config.values:
1190 noallowed.append(k)
1191 for k in rem:
1192 del confoverrides[k]
1193 if len(noallowed) > 0:
1194 raise ValueError( # pragma: no cover
1195 "The following configuration values are declared in any extension.\n--???--\n"
1196 "{0}\n--DECLARED--\n{1}".format(
1197 "\n".join(sorted(noallowed)),
1198 "\n".join(sorted(self.config.values))))
1200 # now that we know all config values, collect them from conf.py
1201 self.config.init_values()
1202 self.events.emit('config-inited', self.config)
1204 # /2 addition to the original code
1205 # check extension versions if requested
1206 # self.config.needs_extensions = self.config.extensions
1207 if not hasattr(self.config, 'items'):
1209 def _citems():
1210 for k, v in self.config.values.items():
1211 yield k, v
1213 self.config.items = _citems
1215 # /2 end of addition
1217 # create the project
1218 self.project = Project(self.srcdir, self.config.source_suffix)
1219 # set up the build environment
1220 self._init_env(freshenv)
1221 assert self.env is not None
1222 # create the builder, initializes _MemoryBuilder
1223 self.builder = self.create_builder(buildername)
1224 # build environment post-initialisation, after creating the builder
1225 if hasattr(self, "_post_init_env"):
1226 self._post_init_env()
1227 # set up the builder
1228 self._init_builder()
1230 if not isinstance(self.env, _CustomBuildEnvironment):
1231 raise TypeError( # pragma: no cover
1232 f"self.env is not _CustomBuildEnvironment: {type(self.env)!r} "
1233 f"buildername='{buildername}'")
1235 # addition
1236 self._extended_init_()
1238 # verification
1239 self._check_init_()
1241 def _init_builder(self) -> None:
1242 if not hasattr(self.builder, "env") or self.builder.env is None:
1243 self.builder.set_environment(self.env)
1244 self.builder.init()
1245 self.events.emit('builder-inited')
1247 def _check_init_(self):
1248 pass
1250 def _init_env(self, freshenv):
1251 ENV_PICKLE_FILENAME = 'environment.pickle'
1252 filename = os.path.join(self.doctreedir, ENV_PICKLE_FILENAME)
1253 if freshenv or not os.path.exists(filename):
1254 self.env = _CustomBuildEnvironment(self)
1255 self._fresh_env_used = True
1256 self.env.setup(self)
1257 if (self.srcdir is not None and self.srcdir != "IMPOSSIBLE:TOFIND" and
1258 self.builder is not None):
1259 self.env.find_files(self.config, self.builder)
1260 return self.env
1262 if "IMPOSSIBLE:TOFIND" not in self.doctreedir: # pragma: no cover
1263 from sphinx.application import ENV_PICKLE_FILENAME
1264 filename = os.path.join(self.doctreedir, ENV_PICKLE_FILENAME)
1265 try:
1266 self.info('loading pickled environment... ')
1267 with open(filename, 'rb') as f:
1268 self.env = pickle.load(f)
1269 self.env.setup(self)
1270 self.info('done')
1271 return self.env
1272 except Exception as err:
1273 self.info('failed: %r', err)
1274 return self._init_env(freshenv=True)
1276 if self.env is None: # pragma: no cover
1277 self.env = _CustomBuildEnvironment(self)
1278 if hasattr(self.env, 'setup'):
1279 self.env.setup(self)
1280 return self.env
1282 if not hasattr(self.env, 'project') or self.env.project is None:
1283 raise AttributeError( # pragma: no cover
1284 "self.env.project is not initialized.")
1286 def create_builder(self, name):
1287 """
1288 Creates a builder, raises an exception if name is None.
1289 """
1290 if name is None:
1291 raise ValueError( # pragma: no cover
1292 "Builder name cannot be None")
1293 try:
1294 return self.registry.create_builder(self, name, env=self.env)
1295 except TypeError:
1296 # old version of sphinx
1297 return self.registry.create_builder(self, name)
1299 def _extended_init_(self):
1300 """
1301 Additional initialization steps.
1302 """
1303 if not hasattr(self, "domains"):
1304 self.domains = {}
1305 if not hasattr(self, "_events"):
1306 self._events = {}
1308 # Otherwise, role issue is missing.
1309 setup_link_roles(self)
1311 def _lookup_doctree(self, doctree, node_type):
1312 for node in doctree.traverse(node_type):
1313 yield node
1315 def _add_missing_ids(self, doctree):
1316 for i, node in enumerate(self._lookup_doctree(doctree, None)):
1317 stype = str(type(node))
1318 if ('section' not in stype and 'title' not in stype and
1319 'reference' not in stype):
1320 continue
1321 try:
1322 node['ids'][0]
1323 except IndexError:
1324 node['ids'] = ['missing%d' % i]
1325 except TypeError: # pragma: no cover
1326 pass
1328 def finalize(self, doctree, external_docnames=None):
1329 """
1330 Finalizes the documentation after it was parsed.
1332 @param doctree doctree (or pub.document), available after publication
1333 @param external_docnames other docnames the doctree references
1334 """
1335 imgs = list(self._lookup_doctree(doctree, nodes.image))
1336 for img in imgs:
1337 img['save_uri'] = img['uri']
1339 if not isinstance(self.env, _CustomBuildEnvironment):
1340 raise TypeError( # pragma: no cover
1341 f"self.env is not _CustomBuildEnvironment: '{type(self.env)}'")
1342 if not isinstance(self.builder.env, _CustomBuildEnvironment):
1343 raise TypeError( # pragma: no cover
1344 "self.builder.env is not _CustomBuildEnvironment: '{0}'".format(
1345 type(self.builder.env)))
1346 self.doctree_ = doctree
1347 self.builder.doctree_ = doctree
1348 self.env.doctree_[self.config.master_doc] = doctree
1349 self.env.all_docs = {self.config.master_doc: self.config.master_doc}
1351 if external_docnames:
1352 for doc in external_docnames:
1353 self.env.all_docs[doc] = doc
1355 # This steps goes through many function including one
1356 # modifying paths in image node.
1357 # Look for node['candidates'] = candidates in Sphinx code.
1358 # If a path startswith('/'), it is removed.
1359 from sphinx.environment.collectors.asset import logger as logger_asset
1360 logger_asset.setLevel(40) # only errors
1361 self._add_missing_ids(doctree)
1362 self.events.emit('doctree-read', doctree)
1363 logger_asset.setLevel(30) # back to warnings
1365 for img in imgs:
1366 img['uri'] = img['save_uri']
1368 self.events.emit('doctree-resolved', doctree,
1369 self.config.master_doc)
1370 self.builder.write(None, None, 'all')
1372 def debug(self, message, *args, **kwargs):
1373 self._logger.debug(message, *args, **kwargs)
1375 def info(self, message, *args):
1376 self._logger.info(message, *args)
1378 def warning(self, message='', name=None, type=None, subtype=None):
1379 if "is already registered" not in message: # pragma: no cover
1380 self._logger.warning(
1381 "[_CustomSphinx] %s -- %s", message, name,
1382 type=type, subtype=subtype)
1384 def add_builder(self, builder, override=False):
1385 self._added_objects.append(('builder', builder))
1386 if builder.name not in self.registry.builders:
1387 self.debug('[_CustomSphinx] adding builder: %r', builder)
1388 self.registry.add_builder(builder, override=override)
1389 else:
1390 self.debug('[_CustomSphinx] already added builder: %r', builder)
1392 def setup_extension(self, extname):
1393 self._added_objects.append(('extension', extname))
1395 logger = getLogger('sphinx.application')
1396 disa = logger.logger.disabled
1397 logger.logger.disabled = True
1399 # delayed import to speed up time
1400 try:
1401 with warnings.catch_warnings():
1402 warnings.filterwarnings(
1403 "ignore", category=DeprecationWarning)
1404 self.registry.load_extension(self, extname)
1405 except Exception as e: # pragma: no cover
1406 raise ExtensionError(
1407 f"Unable to setup extension '{extname}'") from e
1408 finally:
1409 logger.logger = disa
1411 def add_directive(self, name, obj, content=None, arguments=None, # pylint: disable=W0221,W0237
1412 override=True, **options):
1413 self._added_objects.append(('directive', name))
1414 if name == 'plot' and obj.__name__ == 'PlotDirective':
1416 old_run = obj.run
1418 def run(self):
1419 """Run the plot directive."""
1420 logger = getLogger("MockSphinxApp")
1421 logger.info('[MockSphinxApp] PlotDirective: %r', self.content)
1422 try:
1423 res = old_run(self)
1424 logger.info('[MockSphinxApp] PlotDirective ok')
1425 return res
1426 except OSError as e: # pragma: no cover
1427 logger = getLogger("MockSphinxApp")
1428 logger.info('[MockSphinxApp] PlotDirective failed: %s', e)
1429 return []
1431 obj.run = run
1433 Sphinx.add_directive(self, name, obj, override=override, **options)
1435 def add_domain(self, domain, override=True):
1436 self._added_objects.append(('domain', domain))
1437 Sphinx.add_domain(self, domain, override=override)
1438 # For some reason, the directives are missing from the main catalog
1439 # in docutils.
1440 for k, v in domain.directives.items():
1441 self.add_directive(f"{domain.name}:{k}", v)
1442 if domain.name in ('py', 'std', 'rst'):
1443 # We add the directive without the domain name as a prefix.
1444 self.add_directive(k, v)
1445 for k, v in domain.roles.items():
1446 self.add_role(f"{domain.name}:{k}", v)
1447 if domain.name in ('py', 'std', 'rst'):
1448 # We add the role without the domain name as a prefix.
1449 self.add_role(k, v)
1451 def add_role(self, name, role, override=True):
1452 self._added_objects.append(('role', name))
1453 self.debug('[_CustomSphinx] adding role: %r', (name, role))
1454 roles.register_local_role(name, role)
1456 def add_generic_role(self, name, nodeclass, override=True):
1457 self._added_objects.append(('generic_role', name))
1458 self.debug("[_CustomSphinx] adding generic role: '%r'",
1459 (name, nodeclass))
1460 role = roles.GenericRole(name, nodeclass)
1461 roles.register_local_role(name, role)
1463 def add_node(self, node, override=True, **kwds):
1464 self._added_objects.append(('node', node))
1465 self.debug('[_CustomSphinx] adding node: %r', (node, kwds))
1466 nodes._add_node_class_names([node.__name__])
1467 for key, val in kwds.items():
1468 try:
1469 visit, depart = val
1470 except ValueError: # pragma: no cover
1471 raise ExtensionError(("Value for key '%r' must be a "
1472 "(visit, depart) function tuple") % key)
1473 translator = self.registry.translators.get(key)
1474 translators = []
1475 if translator is not None:
1476 translators.append(translator)
1477 elif key == 'html':
1478 from sphinx.writers.html import HTMLTranslator
1479 translators.append(HTMLTranslator)
1480 if is_html5_writer_available():
1481 from sphinx.writers.html5 import HTML5Translator
1482 translators.append(HTML5Translator)
1483 elif key == 'latex':
1484 translators.append(_get_LaTeXTranslator())
1485 elif key == 'elatex':
1486 translators.append(EnhancedLaTeXBuilder)
1487 elif key == 'text':
1488 from sphinx.writers.text import TextTranslator
1489 translators.append(TextTranslator)
1490 elif key == 'man':
1491 from sphinx.writers.manpage import ManualPageTranslator
1492 translators.append(ManualPageTranslator)
1493 elif key == 'texinfo':
1494 from sphinx.writers.texinfo import TexinfoTranslator
1495 translators.append(TexinfoTranslator)
1497 for translator in translators:
1498 setattr(translator, 'visit_' + node.__name__, visit)
1499 if depart:
1500 setattr(translator, 'depart_' + node.__name__, depart)
1502 def add_event(self, name):
1503 self._added_objects.append(('event', name))
1504 Sphinx.add_event(self, name)
1506 def add_config_value(self, name, default, rebuild, types_=(), types=()): # pylint: disable=W0221,W0237
1507 types_ = types or types_
1508 self._added_objects.append(('config_value', name))
1509 Sphinx.add_config_value(self, name, default, rebuild, types_)
1511 def add_directive_to_domain(self, domain, name, obj, has_content=None, # pylint: disable=W0221,W0237
1512 argument_spec=None, override=False, **option_spec):
1513 self._added_objects.append(('directive_to_domain', domain, name))
1514 Sphinx.add_directive_to_domain(self, domain, name, obj,
1515 override=override, **option_spec)
1517 def add_role_to_domain(self, domain, name, role, override=False):
1518 self._added_objects.append(('roles_to_domain', domain, name))
1519 Sphinx.add_role_to_domain(self, domain, name, role, override=override)
1521 def add_transform(self, transform):
1522 self._added_objects.append(('transform', transform))
1523 Sphinx.add_transform(self, transform)
1525 def add_post_transform(self, transform):
1526 self._added_objects.append(('post_transform', transform))
1527 Sphinx.add_post_transform(self, transform)
1529 def add_js_file(self, filename, priority=500, **kwargs): # pylint: disable=W0221
1530 # loading_method=None: added in Sphinx 4.4
1531 self._added_objects.append(('js', filename))
1532 Sphinx.add_js_file(self, filename, priority=priority, **kwargs)
1534 def add_css_file(self, filename, priority=500, **kwargs):
1535 self._added_objects.append(('css', filename))
1536 Sphinx.add_css_file(self, filename, priority=priority, **kwargs)
1538 def add_latex_package(self, packagename, options=None, after_hyperref=False):
1539 self._added_objects.append(('latex', packagename))
1540 Sphinx.add_latex_package(
1541 self, packagename=packagename, options=options,
1542 after_hyperref=after_hyperref)
1544 def add_object_type(self, directivename, rolename, indextemplate='',
1545 parse_node=None, ref_nodeclass=None, objname='',
1546 doc_field_types=None, override=False):
1547 if doc_field_types is None:
1548 doc_field_types = []
1549 self._added_objects.append(('object', directivename, rolename))
1550 Sphinx.add_object_type(self, directivename, rolename, indextemplate=indextemplate,
1551 parse_node=parse_node, ref_nodeclass=ref_nodeclass,
1552 objname=objname, doc_field_types=doc_field_types,
1553 override=override)
1555 def add_env_collector(self, collector):
1556 """
1557 See :epkg:`class Sphinx`.
1558 """
1559 self.debug(
1560 '[_CustomSphinx] adding environment collector: %r', collector)
1561 coll = collector()
1562 coll.enable(self)
1563 self._added_collectors.append(coll)
1565 def disconnect_env_collector(self, clname, exc=True):
1566 """
1567 Disables a collector given its class name.
1569 @param cl name
1570 @param exc raises an exception if not found
1571 @return found collector
1572 """
1573 found = None
1574 foundi = None
1575 for i, co in enumerate(self._added_collectors):
1576 if clname == co.__class__.__name__:
1577 found = co
1578 foundi = i
1579 break
1580 if found is not None and not exc:
1581 return None
1582 if found is None:
1583 raise ValueError( # pragma: no cover
1584 "Unable to find a collector '{0}' in \n{1}".format(
1585 clname, "\n".join(
1586 map(lambda x: x.__class__.__name__,
1587 self._added_collectors))))
1588 for v in found.listener_ids.values():
1589 self.disconnect(v)
1590 del self._added_collectors[foundi]
1591 return found