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

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 

35 

36 

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 

45 

46 

47try: 

48 from sphinx.util.docutils import is_html5_writer_available 

49except ImportError: 

50 def is_html5_writer_available(): 

51 return True 

52 

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 

57 

58 

59def update_docutils_languages(values=None): 

60 """ 

61 Updates ``docutils/languages/en.py`` with missing labels. 

62 It Does it for languages *en*. 

63 

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') 

74 

75 

76class _AdditionalVisitDepart: 

77 """ 

78 Additional visitors and departors. 

79 """ 

80 

81 def __init__(self, output_format): 

82 self.output_format = output_format 

83 

84 def is_html(self): 

85 """ 

86 Tells if the translator is :epkg:`html` format. 

87 """ 

88 return self.base_class is HTMLTranslator 

89 

90 def is_rst(self): 

91 """ 

92 Tells if the translator is :epkg:`rst` format. 

93 """ 

94 return self.base_class is RstTranslator 

95 

96 def is_latex(self): 

97 """ 

98 Tells if the translator is :epkg:`latex` format. 

99 """ 

100 return self.base_class is _get_LaTeXTranslator() 

101 

102 def is_md(self): 

103 """ 

104 Tells if the translator is :epkg:`markdown` format. 

105 """ 

106 return self.base_class is _get_LaTeXTranslator() 

107 

108 def is_doctree(self): 

109 """ 

110 Tells if the translator is doctree format. 

111 """ 

112 return self.base_class is _get_LaTeXTranslator() 

113 

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) 

127 

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 

146 

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 

153 

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 

161 

162 def visit_viewcode_anchor(self, node): 

163 # Removed in sphinx 3.5 

164 pass 

165 

166 def depart_viewcode_anchor(self, node): 

167 # Removed in sphinx 3.5 

168 pass 

169 

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__)) 

174 

175 

176class HTMLTranslatorWithCustomDirectives(_AdditionalVisitDepart, HTMLTranslator): 

177 """ 

178 See @see cl HTMLWriterWithCustomDirectives. 

179 """ 

180 

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 

190 

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) 

196 

197 def visit_pending_xref(self, node): 

198 self.visit_Text(node) 

199 raise nodes.SkipNode 

200 

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__)) 

204 

205 

206class RSTTranslatorWithCustomDirectives(_AdditionalVisitDepart, RstTranslator): 

207 """ 

208 See @see cl HTMLWriterWithCustomDirectives. 

209 """ 

210 

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 

221 

222 

223class MDTranslatorWithCustomDirectives(_AdditionalVisitDepart, MdTranslator): 

224 """ 

225 See @see cl HTMLWriterWithCustomDirectives. 

226 """ 

227 

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 

238 

239 

240class DocTreeTranslatorWithCustomDirectives(DocTreeTranslator): 

241 """ 

242 See @see cl HTMLWriterWithCustomDirectives. 

243 """ 

244 

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 

251 

252 

253class LatexTranslatorWithCustomDirectives(_AdditionalVisitDepart, EnhancedLaTeXTranslator): 

254 """ 

255 See @see cl LatexWriterWithCustomDirectives. 

256 """ 

257 

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 

274 

275 

276class _WriterWithCustomDirectives: 

277 """ 

278 Common class to @see cl HTMLWriterWithCustomDirectives and @see cl RSTWriterWithCustomDirectives. 

279 """ 

280 

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 

299 

300 def connect_directive_node(self, name, f_visit, f_depart): 

301 """ 

302 Adds custom node to the translator. 

303 

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)) 

310 

311 def add_configuration_options(self, new_options): 

312 """ 

313 Add new options. 

314 

315 @param new_options new options 

316 """ 

317 for k, v in new_options.items(): 

318 self.builder.config.values[k] = v 

319 

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). 

326 

327 Normally not overridden or extended in subclasses. 

328 """ 

329 self.base_class.write(self, document, destination) 

330 

331 

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. 

337 

338 See `Write your own ReStructuredText-Writer <http://www.arnebrodowski.de/blog/write-your-own-restructuredtext-writer.html>`_. 

339 

340 This class needs to tell :epkg:`docutils` to call the added function 

341 when directives *runpython* or *blogpost* are met. 

342 """ 

343 

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) 

351 

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:]) 

364 

365 

366class RSTWriterWithCustomDirectives(_WriterWithCustomDirectives, RstWriter): 

367 """ 

368 This :epkg:`docutils` writer extends the :epkg:`RST` writer with 

369 custom directives implemented in this module. 

370 """ 

371 

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) 

379 

380 def translate(self): 

381 visitor = self.translator_class(self.document, self.builder) 

382 self.document.walkabout(visitor) 

383 self.output = visitor.body 

384 

385 

386class MDWriterWithCustomDirectives(_WriterWithCustomDirectives, MdWriter): 

387 """ 

388 This :epkg:`docutils` writer extends the :epkg:`MD` writer with 

389 custom directives implemented in this module. 

390 """ 

391 

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) 

399 

400 def translate(self): 

401 visitor = self.translator_class(self.document, self.builder) 

402 self.document.walkabout(visitor) 

403 self.output = visitor.body 

404 

405 

406class DocTreeWriterWithCustomDirectives(_WriterWithCustomDirectives, DocTreeWriter): 

407 """ 

408 This :epkg:`docutils` writer creates a doctree writer with 

409 custom directives implemented in this module. 

410 """ 

411 

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) 

419 

420 def translate(self): 

421 visitor = self.translator_class(self.document, self.builder) 

422 self.document.walkabout(visitor) 

423 self.output = visitor.body 

424 

425 

426class LatexWriterWithCustomDirectives(_WriterWithCustomDirectives, EnhancedLaTeXWriter): 

427 """ 

428 This :epkg:`docutils` writer extends the :epkg:`Latex` writer with 

429 custom directives implemented in this module. 

430 """ 

431 

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)}") 

442 

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 

459 

460 

461class _MemoryBuilder: 

462 """ 

463 Builds :epkg:`HTML` output in memory. 

464 The API is defined by the page 

465 :epkg:`builderapi`. 

466 """ 

467 

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). 

473 

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 

483 

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 

491 

492 def iter_pages(self): 

493 """ 

494 Enumerate created pages. 

495 

496 @return iterator on tuple(name, content) 

497 """ 

498 for k, v in self.built_pages.items(): 

499 yield k, v.getvalue() 

500 

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) 

509 

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) 

525 

526 def _write_parallel(self, docnames, nproc): 

527 """ 

528 Not supported. 

529 """ 

530 raise NotImplementedError( 

531 "Use parallel=0 when creating the sphinx application.") 

532 

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 

551 

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:] 

567 

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}") 

582 

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("\\", "/") 

588 

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] 

607 

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 

619 

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 

630 

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 

640 

641 ctx['toctree'] = lambda **kw: self._get_local_toctree(pagename, **kw) 

642 self.add_sidebars(pagename, ctx) 

643 ctx.update(addctx) 

644 

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 

650 

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 

659 

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) 

667 

668 

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 

685 

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). 

691 

692 :param app: :epkg:`Sphinx application` 

693 """ 

694 _MemoryBuilder._init(self, CustomSingleFileHTMLBuilder, app, env=env) 

695 

696 

697class MemoryRSTBuilder(_MemoryBuilder, RstBuilder): 

698 

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 """ 

706 

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 

717 

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). 

723 

724 :param app: :epkg:`Sphinx application` 

725 """ 

726 _MemoryBuilder._init(self, RstBuilder, app, env=env) 

727 

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) 

741 

742 

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 

759 

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). 

765 

766 :param app: :epkg:`Sphinx application` 

767 """ 

768 _MemoryBuilder._init(self, MdBuilder, app, env=env) 

769 

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) 

783 

784 

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 

800 

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). 

806 

807 :param app: :epkg:`Sphinx application` 

808 """ 

809 _MemoryBuilder._init(self, DocTreeBuilder, app, env=env) 

810 

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) 

824 

825 

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 

842 

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). 

848 

849 :param app: :epkg:`Sphinx application` 

850 """ 

851 _MemoryBuilder._init(self, EnhancedLaTeXBuilder, app, env=env) 

852 

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)) 

864 

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) 

872 

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 

880 

881 

882class _CustomBuildEnvironment(BuildEnvironment): 

883 """ 

884 Overrides some functionalities of 

885 `BuildEnvironment <https://www.sphinx-doc.org/en/master/extdev/envapi.html>`_. 

886 """ 

887 

888 def __init__(self, app): 

889 """ 

890 """ 

891 BuildEnvironment.__init__(self, app) 

892 self.doctree_ = {} 

893 

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 

903 

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))) 

913 

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) 

919 

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 

924 

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() 

930 

931 

932class _CustomSphinx(Sphinx): 

933 """ 

934 Custom :epkg:`Sphinx` application to avoid using disk. 

935 """ 

936 

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: 

945 

946 @param new_extensions extensions to add to the application 

947 

948 Some insights about domains: 

949 

950 :: 

951 

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} 

960 

961 And builders: 

962 

963 :: 

964 

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 = [] 

997 

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 

1004 

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 

1015 

1016 if doctreedir is None: 

1017 doctreedir = "IMPOSSIBLE:TOFIND" 

1018 if srcdir is None: 

1019 srcdir = "IMPOSSIBLE:TOFIND" 

1020 update_docutils_languages() 

1021 

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 

1028 

1029 if self.srcdir == self.outdir: 

1030 raise ApplicationError('Source directory and destination ' # pragma: no cover 

1031 'directory cannot be identical') 

1032 

1033 if status is None: 

1034 self._status = StringIO() 

1035 self.quiet = True 

1036 else: 

1037 self._status = status 

1038 self.quiet = False 

1039 

1040 from sphinx.events import EventManager 

1041 # logging.setup(self, self._status, self._warning) 

1042 self.events = EventManager(self) 

1043 

1044 # keep last few messages for traceback 

1045 # This will be filled by sphinx.util.logging.LastMessagesWriter 

1046 self.messagelog = deque(maxlen=10) 

1047 

1048 # say hello to the world 

1049 from sphinx import __display_version__ 

1050 self.info(f'Running Sphinx v{__display_version__}') # pragma: no cover 

1051 

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") 

1058 

1059 # status code for command-line application 

1060 self.statuscode = 0 

1061 

1062 # delayed import to speed up time 

1063 from sphinx.application import builtin_extensions 

1064 from sphinx.config import CONFIG_FILENAME, Config, Tags 

1065 

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__ 

1089 

1090 # create the environment 

1091 self.config.pre_init_values() 

1092 

1093 # set up translation infrastructure 

1094 self._init_i18n() 

1095 

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) 

1104 

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 

1109 

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 

1122 

1123 # load all user-given extension modules 

1124 for extension in self.config.extensions: 

1125 self.setup_extension(extension) 

1126 

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] 

1139 

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) 

1146 

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] 

1154 

1155 # /1 end of addition 

1156 

1157 # preload builder module (before init config values) 

1158 self.preload_builder(buildername) 

1159 

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) 

1177 

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)))) 

1199 

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) 

1203 

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'): 

1208 

1209 def _citems(): 

1210 for k, v in self.config.values.items(): 

1211 yield k, v 

1212 

1213 self.config.items = _citems 

1214 

1215 # /2 end of addition 

1216 

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() 

1229 

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}'") 

1234 

1235 # addition 

1236 self._extended_init_() 

1237 

1238 # verification 

1239 self._check_init_() 

1240 

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') 

1246 

1247 def _check_init_(self): 

1248 pass 

1249 

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 

1261 

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) 

1275 

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 

1281 

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.") 

1285 

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) 

1298 

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 = {} 

1307 

1308 # Otherwise, role issue is missing. 

1309 setup_link_roles(self) 

1310 

1311 def _lookup_doctree(self, doctree, node_type): 

1312 for node in doctree.traverse(node_type): 

1313 yield node 

1314 

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 

1327 

1328 def finalize(self, doctree, external_docnames=None): 

1329 """ 

1330 Finalizes the documentation after it was parsed. 

1331 

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'] 

1338 

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} 

1350 

1351 if external_docnames: 

1352 for doc in external_docnames: 

1353 self.env.all_docs[doc] = doc 

1354 

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 

1364 

1365 for img in imgs: 

1366 img['uri'] = img['save_uri'] 

1367 

1368 self.events.emit('doctree-resolved', doctree, 

1369 self.config.master_doc) 

1370 self.builder.write(None, None, 'all') 

1371 

1372 def debug(self, message, *args, **kwargs): 

1373 self._logger.debug(message, *args, **kwargs) 

1374 

1375 def info(self, message, *args): 

1376 self._logger.info(message, *args) 

1377 

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) 

1383 

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) 

1391 

1392 def setup_extension(self, extname): 

1393 self._added_objects.append(('extension', extname)) 

1394 

1395 logger = getLogger('sphinx.application') 

1396 disa = logger.logger.disabled 

1397 logger.logger.disabled = True 

1398 

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 

1410 

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': 

1415 

1416 old_run = obj.run 

1417 

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 [] 

1430 

1431 obj.run = run 

1432 

1433 Sphinx.add_directive(self, name, obj, override=override, **options) 

1434 

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) 

1450 

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) 

1455 

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) 

1462 

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) 

1496 

1497 for translator in translators: 

1498 setattr(translator, 'visit_' + node.__name__, visit) 

1499 if depart: 

1500 setattr(translator, 'depart_' + node.__name__, depart) 

1501 

1502 def add_event(self, name): 

1503 self._added_objects.append(('event', name)) 

1504 Sphinx.add_event(self, name) 

1505 

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_) 

1510 

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) 

1516 

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) 

1520 

1521 def add_transform(self, transform): 

1522 self._added_objects.append(('transform', transform)) 

1523 Sphinx.add_transform(self, transform) 

1524 

1525 def add_post_transform(self, transform): 

1526 self._added_objects.append(('post_transform', transform)) 

1527 Sphinx.add_post_transform(self, transform) 

1528 

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) 

1533 

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) 

1537 

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) 

1543 

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) 

1554 

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) 

1564 

1565 def disconnect_env_collector(self, clname, exc=True): 

1566 """ 

1567 Disables a collector given its class name. 

1568 

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