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