Coverage for pyquickhelper/sphinxext/sphinx_runpython_extension.py: 91%

301 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-03 02:21 +0200

1# -*- coding: utf-8 -*- 

2""" 

3@file 

4@brief Defines runpython directives. 

5See `Tutorial: Writing a simple extension 

6<https://www.sphinx-doc.org/en/master/development/tutorials/helloworld.html>`_ 

7""" 

8import sys 

9import os 

10from contextlib import redirect_stdout, redirect_stderr 

11import traceback 

12import warnings 

13from io import StringIO 

14import sphinx 

15from docutils import nodes, core 

16from docutils.parsers.rst import Directive, directives 

17from docutils.statemachine import StringList 

18from sphinx.util.nodes import nested_parse_with_titles 

19from sphinx.util import logging 

20from ..loghelper.flog import run_cmd 

21from ..texthelper.texts_language import TITLES 

22from ..pycode.code_helper import remove_extra_spaces_and_pep8 

23from .sphinx_collapse_extension import collapse_node 

24 

25 

26class RunPythonCompileError(Exception): 

27 """ 

28 exception raised when a piece of code 

29 included in the documentation does not compile 

30 """ 

31 pass 

32 

33 

34class RunPythonExecutionError(Exception): 

35 """ 

36 Exception raised when a piece of code 

37 included in the documentation raises an exception. 

38 """ 

39 pass 

40 

41 

42def run_python_script(script, params=None, comment=None, setsysvar=None, process=False, 

43 exception=False, warningout=None, chdir=None, context=None, 

44 store_in_file=None): 

45 """ 

46 Executes a script :epkg:`python` as a string. 

47 

48 @param script python script 

49 @param params params to add before the execution 

50 @param comment message to add in a exception when the script fails 

51 @param setsysvar if not None, add a member to module *sys*, 

52 set up this variable to True, 

53 if is remove after the execution 

54 @param process run the script in a separate process 

55 @param exception expects an exception to be raised, 

56 fails if it is not, the function returns no output and the 

57 error message 

58 @param warningout warning to disable (name of warnings) 

59 @param chdir change directory before running this script (if not None) 

60 @param context if not None, added to the local context 

61 @param store_in_file stores the script into this file 

62 and calls tells python the source can be found here, 

63 that is useful is the script is using module 

64 ``inspect`` to retrieve the source which are not 

65 stored in memory 

66 @return stdout, stderr, context 

67 

68 If the execution throws an exception such as 

69 ``NameError: name 'math' is not defined`` after importing 

70 the module ``math``. It comes from the fact 

71 the domain name used by the function 

72 `exec <https://docs.python.org/3/library/functions.html#exec>`_ 

73 contains the declared objects. Example: 

74 

75 :: 

76 

77 import math 

78 def coordonnees_polaires(x,y): 

79 rho = math.sqrt(x*x+y*y) 

80 theta = math.atan2 (y,x) 

81 return rho, theta 

82 coordonnees_polaires(1, 1) 

83 

84 The code can be modified into: 

85 

86 :: 

87 

88 def fake_function(): 

89 import math 

90 def coordonnees_polaires(x,y): 

91 rho = math.sqrt(x*x+y*y) 

92 theta = math.atan2 (y,x) 

93 return rho, theta 

94 coordonnees_polaires(1, 1) 

95 fake_function() 

96 

97 Section :ref:`l-image-rst-runpython` explains 

98 how to display an image with this directive. 

99 """ 

100 def warning_filter(warningout): 

101 if warningout in (None, ''): 

102 warnings.simplefilter("always") 

103 elif isinstance(warningout, str): 

104 li = [_.strip() for _ in warningout.split()] 

105 warning_filter(li) 

106 elif isinstance(warningout, list): 

107 def interpret(s): 

108 return eval(s) if isinstance(s, str) else s 

109 warns = [interpret(w) for w in warningout] 

110 for w in warns: 

111 warnings.simplefilter("ignore", w) 

112 else: 

113 raise ValueError( 

114 f"Unexpected value for warningout: {warningout}") 

115 

116 if params is None: 

117 params = {} 

118 

119 if process: 

120 if context is not None and len(context) != 0: 

121 raise RunPythonExecutionError( # pragma: no cover 

122 "context cannot be used if the script runs in a separate process.") 

123 

124 cmd = sys.executable 

125 header = ["# coding: utf-8", "import sys"] 

126 if setsysvar: 

127 header.append(f"sys.{setsysvar} = True") 

128 add = 0 

129 for path in sys.path: 

130 if path.endswith("source") or path.endswith("source/") or path.endswith("source\\"): 

131 header.append("sys.path.append('{0}')".format( 

132 path.replace("\\", "\\\\"))) 

133 add += 1 

134 if add == 0: 

135 for path in sys.path: 

136 if path.endswith("src") or path.endswith("src/") or path.endswith("src\\"): 

137 header.append("sys.path.append('{0}')".format( 

138 path.replace("\\", "\\\\"))) 

139 add += 1 

140 if add == 0: 

141 # It did not find any path linked to the copy of 

142 # the current module in the documentation 

143 # it assumes the first path of `sys.path` is part 

144 # of the unit test. 

145 path = sys.path[0] 

146 path = os.path.join(path, "..", "..", "src") 

147 if os.path.exists(path): 

148 header.append("sys.path.append('{0}')".format( 

149 path.replace("\\", "\\\\"))) 

150 add += 1 

151 else: 

152 path = sys.path[0] 

153 path = os.path.join(path, "src") 

154 if os.path.exists(path): 

155 header.append("sys.path.append('{0}')".format( 

156 path.replace("\\", "\\\\"))) 

157 add += 1 

158 

159 if add == 0: 

160 # We do nothing unless the execution failed. 

161 exc_path = RunPythonExecutionError( 

162 "Unable to find a path to add:\n{0}".format("\n".join(sys.path))) 

163 else: 

164 exc_path = None 

165 header.append('') 

166 script = "\n".join(header) + script 

167 

168 if store_in_file is not None: 

169 with open(store_in_file, "w", encoding="utf-8") as f: 

170 f.write(script) 

171 script_arg = None 

172 cmd += ' ' + store_in_file 

173 else: 

174 script_arg = script 

175 

176 try: 

177 out, err = run_cmd(cmd, script_arg, wait=True, change_path=chdir) 

178 return out, err, None 

179 except Exception as ee: # pragma: no cover 

180 if not exception: 

181 message = ("--SCRIPT--\n{0}\n--PARAMS--\n{1}\n--COMMENT--\n" 

182 "{2}\n--ERR--\n{3}\n--OUT--\n{4}\n--EXC--\n{5}" 

183 "").format(script, params, comment, "", 

184 str(ee), ee) 

185 if exc_path: 

186 message += f"\n---EXC--\n{exc_path}" 

187 raise RunPythonExecutionError(message) from ee 

188 return str(ee), str(ee), None 

189 else: 

190 if store_in_file: 

191 raise NotImplementedError( 

192 "store_in_file is only implemented if process is True.") 

193 try: 

194 obj = compile(script, "", "exec") 

195 except Exception as ec: # pragma: no cover 

196 if comment is None: 

197 comment = "" 

198 if not exception: 

199 message = f"SCRIPT:\n{script}\nPARAMS\n{params}\nCOMMENT\n{comment}" 

200 raise RunPythonCompileError(message) from ec 

201 return "", f"Cannot compile the do to {ec}", None 

202 

203 globs = globals().copy() 

204 loc = locals() 

205 for k, v in params.items(): 

206 loc[k] = v 

207 loc["__dict__"] = params 

208 if context is not None: 

209 for k, v in context.items(): 

210 globs["__runpython__" + k] = v 

211 globs['__runpython__script__'] = script 

212 

213 if setsysvar is not None: 

214 sys.__dict__[setsysvar] = True 

215 

216 sout = StringIO() 

217 serr = StringIO() 

218 with redirect_stdout(sout): 

219 with redirect_stderr(sout): 

220 

221 with warnings.catch_warnings(): 

222 warning_filter(warningout) 

223 

224 if chdir is not None: 

225 current = os.getcwd() 

226 os.chdir(chdir) 

227 

228 try: 

229 exec(obj, globs, loc) 

230 except Exception as ee: 

231 if chdir is not None: 

232 os.chdir(current) 

233 if setsysvar is not None: 

234 del sys.__dict__[setsysvar] 

235 if comment is None: 

236 comment = "" 

237 gout = sout.getvalue() 

238 gerr = serr.getvalue() 

239 

240 excs = traceback.format_exc() 

241 lines = excs.split("\n") 

242 excs = "\n".join( 

243 _ for _ in lines if "sphinx_runpython_extension.py" not in _) 

244 

245 if not exception: 

246 message = ("--SCRIPT--\n{0}\n--PARAMS--\n{1}\n--COMMENT--" 

247 "\n{2}\n--ERR--\n{3}\n--OUT--\n{4}\n--EXC--" 

248 "\n{5}\n--TRACEBACK--\n{6}").format( 

249 script, params, comment, gout, gerr, 

250 ee, excs) 

251 raise RunPythonExecutionError(message) from ee 

252 return (gout + "\n" + gerr), (gerr + "\n" + excs), None 

253 

254 if chdir is not None: 

255 os.chdir(current) 

256 

257 if setsysvar is not None: 

258 del sys.__dict__[setsysvar] 

259 

260 gout = sout.getvalue() 

261 gerr = serr.getvalue() 

262 avoid = {"__runpython____WD__", 

263 "__runpython____k__", "__runpython____w__"} 

264 context = {k[13:]: v for k, v in globs.items() if k.startswith( 

265 "__runpython__") and k not in avoid} 

266 return gout, gerr, context 

267 

268 

269class runpython_node(nodes.Structural, nodes.Element): 

270 

271 """ 

272 Defines *runpython* node. 

273 """ 

274 pass 

275 

276 

277class RunPythonDirective(Directive): 

278 

279 """ 

280 Extracts script to run described by ``.. runpython::`` 

281 and modifies the documentation. 

282 

283 .. exref:: 

284 :title: A python script which generates documentation 

285 

286 The following code prints the version of Python 

287 on the standard output. It is added to the documentation:: 

288 

289 .. runpython:: 

290 :showcode: 

291 

292 import sys 

293 print("sys.version_info=", str(sys.version_info)) 

294 

295 If give the following results: 

296 

297 .. runpython:: 

298 

299 import sys 

300 print("sys.version_info=", str(sys.version_info)) 

301 

302 Options *showcode* can be used to display the code. 

303 The option *rst* will assume the output is in RST format and must be 

304 interpreted. *showout* will complement the RST output with the raw format. 

305 

306 The directive has a couple of options: 

307 

308 * ``:assert:`` condition to validate at the end of the execution 

309 to check it went right 

310 * ``:current:`` runs the script in the source file directory 

311 * ``:exception:`` the code throws an exception but it is expected. The error is displayed. 

312 * ``:indent:<int>`` to indent the output 

313 * ``:language:``: changes ``::`` into ``.. code-block:: language`` 

314 * ``:linenos:`` to show line numbers 

315 * ``:nopep8:`` if present, leaves the code as it is and does not apply pep8 by default, 

316 see @see fn remove_extra_spaces_and_pep8. 

317 * ``:numpy_precision: <precision>``, run ``numpy.set_printoptions(precision=...)``, 

318 precision is 3 by default 

319 * ``:process:`` run the script in an another process 

320 * ``:restore:`` restore the local context stored in :epkg:`sphinx` application 

321 by the previous call to *runpython* 

322 * ``:rst:`` to interpret the output, otherwise, it is considered as raw text 

323 * ``:setsysvar:`` adds a member to *sys* module, the module can act differently based on that information, 

324 if the value is left empty, *sys.enable_disabled_documented_pieces_of_code* will be be set up to *True*. 

325 * ``:showcode:`` to show the code before its output 

326 * ``:showout`` if *:rst:* is set up, this flag adds the raw rst output to check what is happening 

327 * ``:sin:<text_for_in>`` which text to display before the code (by default *In*) 

328 * ``:sout:<text_for_in>`` which text to display before the output (by default *Out*) 

329 * ``:sphinx:`` by default, function `nested_parse_with_titles 

330 <https://www.sphinx-doc.org/en/master/extdev/markupapi.html?highlight=nested_parse#parsing-directive-content-as-rest>`_ is 

331 used to parse the output of the script, if this option is set to false, 

332 `public_doctree <http://code.nabla.net/doc/docutils/api/docutils/core/docutils.core.publish_doctree.html>`_. 

333 * ``:store:`` stores the local context in :epkg:`sphinx` application to restore it later 

334 by another call to *runpython* 

335 * ``:toggle:`` add a button to hide or show the code, it takes the values 

336 ``code`` or ``out`` or ``both``. The direction then hides the given section 

337 but adds a button to show it. 

338 * ``:warningout:`` name of warnings to disable (ex: ``ImportWarning``), 

339 separated by spaces 

340 * ``:store_in_file:`` the directive store the script in a file, 

341 then executes this file (only if ``:process:`` is enabled), 

342 this trick is needed when the script to executes relies on 

343 function such :epkg:`*py:inspect:getsource` which requires 

344 the script to be stored somewhere in order to retrieve it. 

345 

346 Option *rst* can be used the following way:: 

347 

348 .. runpython:: 

349 :rst: 

350 

351 for l in range(0,10): 

352 print("**line**", "*" +str(l)+"*") 

353 print('') 

354 

355 Which displays interpreted :epkg:`RST`: 

356 

357 .. runpython:: 

358 :rst: 

359 

360 for l in range(0,10): 

361 print("**line**", "*" +str(l)+"*") 

362 print('') 

363 

364 If the directive produces RST text to be included later in the documentation, 

365 it is able to interpret 

366 `docutils directives <http://docutils.sourceforge.net/docs/ref/rst/directives.html>`_ 

367 and `Sphinx directives 

368 <https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html>`_ 

369 with function `nested_parse_with_titles <http://sphinx-doc.org/extdev/ 

370 markupapi.html?highlight=nested_parse>`_. However, if this text contains 

371 titles, it is better to use option ``:sphinx: false``. 

372 Unless *process* option is enabled, global variables cannot be used. 

373 `sphinx-autorun <https://pypi.org/project/sphinx-autorun/>`_ offers a similar 

374 service except it cannot produce compile :epkg:`RST` content, 

375 hide the source and a couple of other options. 

376 Option *toggle* can hide or unhide the piece of code 

377 or/and its output. 

378 The directive also adds local variables such as 

379 ``__WD__`` which contains the path to the documentation 

380 which contains the directive. It is useful to load additional 

381 files ``os.path.join(__WD__, ...)``. 

382 

383 .. runpython:: 

384 :toggle: out 

385 :showcode: 

386 

387 print("Hide or unhide this output.") 

388 """ 

389 required_arguments = 0 

390 optional_arguments = 0 

391 final_argument_whitespace = True 

392 option_spec = { 

393 'indent': directives.unchanged, 

394 'showcode': directives.unchanged, 

395 'showout': directives.unchanged, 

396 'rst': directives.unchanged, 

397 'sin': directives.unchanged, 

398 'sout': directives.unchanged, 

399 'sphinx': directives.unchanged, 

400 'sout2': directives.unchanged, 

401 'setsysvar': directives.unchanged, 

402 'process': directives.unchanged, 

403 'exception': directives.unchanged, 

404 'nopep8': directives.unchanged, 

405 'warningout': directives.unchanged, 

406 'toggle': directives.unchanged, 

407 'current': directives.unchanged, 

408 'assert': directives.unchanged, 

409 'language': directives.unchanged, 

410 'store': directives.unchanged, 

411 'restore': directives.unchanged, 

412 'numpy_precision': directives.unchanged, 

413 'store_in_file': directives.unchanged, 

414 'linenos': directives.unchanged, 

415 } 

416 has_content = True 

417 runpython_class = runpython_node 

418 

419 def run(self): 

420 """ 

421 Extracts the information in a dictionary, 

422 runs the script. 

423 

424 @return a list of nodes 

425 """ 

426 # settings 

427 sett = self.state.document.settings 

428 language_code = sett.language_code 

429 lineno = self.lineno 

430 

431 # add the instance to the global settings 

432 if hasattr(sett, "out_runpythonlist"): 

433 sett.out_runpythonlist.append(self) 

434 

435 # env 

436 if hasattr(self.state.document.settings, "env"): 

437 env = self.state.document.settings.env 

438 else: 

439 env = None 

440 

441 if env is None: 

442 docname = "___unknown_docname___" 

443 else: 

444 docname = env.docname 

445 

446 # post 

447 bool_set = (True, 1, "True", "1", "true") 

448 bool_set_ = (True, 1, "True", "1", "true", '') 

449 p = { 

450 'showcode': 'showcode' in self.options, 

451 'linenos': 'linenos' in self.options, 

452 'showout': 'showout' in self.options, 

453 'rst': 'rst' in self.options, 

454 'sin': self.options.get('sin', TITLES[language_code]["In"]), 

455 'sout': self.options.get('sout', TITLES[language_code]["Out"]), 

456 'sout2': self.options.get('sout2', TITLES[language_code]["Out2"]), 

457 'sphinx': 'sphinx' not in self.options or self.options['sphinx'] in bool_set, 

458 'setsysvar': self.options.get('setsysvar', None), 

459 'process': 'process' in self.options and self.options['process'] in bool_set_, 

460 'exception': 'exception' in self.options and self.options['exception'] in bool_set_, 

461 'nopep8': 'nopep8' in self.options and self.options['nopep8'] in bool_set_, 

462 'warningout': self.options.get('warningout', '').strip(), 

463 'toggle': self.options.get('toggle', '').strip(), 

464 'current': 'current' in self.options and self.options['current'] in bool_set_, 

465 'assert': self.options.get('assert', '').strip(), 

466 'language': self.options.get('language', '').strip(), 

467 'store_in_file': self.options.get('store_in_file', None), 

468 'numpy_precision': self.options.get('numpy_precision', '3').strip(), 

469 'store': 'store' in self.options and self.options['store'] in bool_set_, 

470 'restore': 'restore' in self.options and self.options['restore'] in bool_set_, 

471 } 

472 

473 if p['setsysvar'] is not None and len(p['setsysvar']) == 0: 

474 p['setsysvar'] = 'enable_disabled_documented_pieces_of_code' 

475 dind = 0 if p['rst'] else 4 

476 p['indent'] = int(self.options.get("indent", dind)) 

477 

478 # run the script 

479 name = f"run_python_script_{id(p)}" 

480 if p['process']: 

481 content = ["if True:"] 

482 else: 

483 content = [f"def {name}():"] 

484 

485 if "numpy" in "\n".join(self.content) and p['numpy_precision'] not in (None, 'None', '-', ''): 

486 try: 

487 import numpy # pylint: disable=W0611 

488 prec = int(p['numpy_precision']) 

489 content.append(" import numpy") 

490 content.append(" numpy.set_printoptions(%d)" % prec) 

491 except (ImportError, ValueError): # pragma: no cover 

492 pass 

493 

494 content.append(' ## __WD__ ##') 

495 

496 if p["restore"]: 

497 context = getattr(env, "runpython_context", None) 

498 for k in sorted(context): 

499 content.append( 

500 " {0} = globals()['__runpython__{0}']".format(k)) 

501 else: 

502 context = None 

503 

504 modified_content = self.modify_script_before_running( 

505 "\n".join(self.content)) 

506 

507 if p['assert']: 

508 footer = [] 

509 assert_condition = p['assert'].split('\n') 

510 for cond in assert_condition: 

511 footer.append(f"if not({cond}):") 

512 footer.append( 

513 f" raise AssertionError('''Condition '{cond}' failed.''')") 

514 modified_content += "\n\n" + "\n".join(footer) 

515 

516 for line in modified_content.split("\n"): 

517 content.append(" " + line) 

518 

519 if p["store"]: 

520 content.append(' for __k__, __v__ in locals().copy().items():') 

521 content.append( 

522 " globals()['__runpython__' + __k__] = __v__") 

523 

524 if not p['process']: 

525 content.append(f"{name}()") 

526 

527 script = "\n".join(content) 

528 script_disp = "\n".join(self.content) 

529 if not p["nopep8"]: 

530 try: 

531 script_disp = remove_extra_spaces_and_pep8( 

532 script_disp, is_string=True) 

533 except Exception as e: # pragma: no cover 

534 logger = logging.getLogger(__name__) 

535 if '.' in docname: 

536 comment = f' File "{docname}", line {lineno}' 

537 else: 

538 comment = ' File "{0}.rst", line {1}\n File "{0}.py", line {1}\n'.format( 

539 docname, lineno) 

540 logger.warning( 

541 f"Pep8 ({e}) issue with {docname!r}\n---SCRIPT---\n{script}") 

542 

543 # if an exception is raised, the documentation should report a warning 

544 # return [document.reporter.warning('messagr', line=self.lineno)] 

545 current_source = self.state.document.current_source 

546 docstring = ":docstring of " in current_source 

547 if docstring: 

548 current_source = current_source.split(":docstring of ")[0] 

549 if os.path.exists(current_source): 

550 comment = f' File "{current_source}", line {lineno}' 

551 if docstring: 

552 new_name = os.path.split(current_source)[0] + ".py" 

553 comment += f'\n File "{new_name}", line {lineno}' 

554 cs_source = current_source 

555 else: 

556 if '.' in docname: 

557 comment = f' File "{docname}", line {lineno}' 

558 else: 

559 comment = ' File "{0}.rst", line {1}\n File "{0}.py", line {1}\n'.format( 

560 docname, lineno) 

561 cs_source = docname 

562 

563 # Add __WD__. 

564 cs_source_dir = os.path.dirname(cs_source).replace("\\", "/") 

565 script = script.replace( 

566 '## __WD__ ##', f"__WD__ = '{cs_source_dir}'") 

567 

568 out, err, context = run_python_script(script, comment=comment, setsysvar=p['setsysvar'], 

569 process=p["process"], exception=p['exception'], 

570 warningout=p['warningout'], 

571 chdir=cs_source_dir if p['current'] else None, 

572 context=context, store_in_file=p['store_in_file']) 

573 

574 if p['store']: 

575 # Stores modified local context. 

576 setattr(env, "runpython_context", context) 

577 else: 

578 context = {} 

579 setattr(env, "runpython_context", context) 

580 

581 if out is not None: 

582 out = out.rstrip(" \n\r\t") 

583 if err is not None: 

584 err = err.rstrip(" \n\r\t") 

585 content = out 

586 if len(err) > 0: 

587 content += "\n[runpythonerror]\n" + err 

588 

589 # add member 

590 self.exe_class = p.copy() 

591 self.exe_class.update(dict(out=out, err=err, script=script)) 

592 

593 # add indent 

594 def add_indent(content, nbind): 

595 "local function" 

596 lines = content.split("\n") 

597 if nbind > 0: 

598 lines = [(" " * nbind + _) for _ in lines] 

599 content = "\n".join(lines) 

600 return content 

601 

602 content = add_indent(content, p['indent']) 

603 

604 # build node 

605 node = self.__class__.runpython_class(rawsource=content, indent=p["indent"], 

606 showcode=p["showcode"], rst=p["rst"], 

607 sin=p["sin"], sout=p["sout"]) 

608 

609 if p["showcode"]: 

610 if 'code' in p['toggle'] or 'both' in p['toggle']: 

611 hide = TITLES[language_code]['hide'] + \ 

612 ' ' + TITLES[language_code]['code'] 

613 unhide = TITLES[language_code]['unhide'] + \ 

614 ' ' + TITLES[language_code]['code'] 

615 secin = collapse_node(hide=hide, unhide=unhide, show=False) 

616 node += secin 

617 else: 

618 secin = node 

619 pin = nodes.paragraph(text=p["sin"]) 

620 if p['language'] in (None, ''): 

621 p['language'] = 'python' 

622 if p['language']: 

623 pcode = nodes.literal_block( 

624 script_disp, script_disp, language=p['language'], 

625 linenos=p['linenos']) 

626 else: 

627 pcode = nodes.literal_block( 

628 script_disp, script_disp, linenos=p['linenos']) 

629 secin += pin 

630 secin += pcode 

631 

632 elif len(self.options.get('sout', '')) == 0: 

633 p["sout"] = '' 

634 p["sout2"] = '' 

635 

636 # RST output. 

637 if p["rst"]: 

638 settings_overrides = {} 

639 try: 

640 sett.output_encoding 

641 except KeyError: # pragma: no cover 

642 settings_overrides["output_encoding"] = "unicode" 

643 # try: 

644 # sett.doctitle_xform 

645 # except KeyError: 

646 # settings_overrides["doctitle_xform"] = True 

647 try: 

648 sett.warning_stream 

649 except KeyError: # pragma: no cover 

650 settings_overrides["warning_stream"] = StringIO() 

651 # 'initial_header_level': 2, 

652 

653 secout = node 

654 if 'out' in p['toggle'] or 'both' in p['toggle']: 

655 hide = TITLES[language_code]['hide'] + \ 

656 ' ' + TITLES[language_code]['outl'] 

657 unhide = TITLES[language_code]['unhide'] + \ 

658 ' ' + TITLES[language_code]['outl'] 

659 secout = collapse_node(hide=hide, unhide=unhide, show=False) 

660 node += secout 

661 elif len(p["sout"]) > 0: 

662 secout += nodes.paragraph(text=p["sout"]) 

663 

664 try: 

665 if p['sphinx']: 

666 st = StringList(content.replace("\r", "").split("\n")) 

667 nested_parse_with_titles(self.state, st, secout) 

668 dt = None 

669 else: 

670 dt = core.publish_doctree( 

671 content, settings=sett, 

672 settings_overrides=settings_overrides) 

673 except Exception as e: # pragma: no cover 

674 tab = content 

675 content = ["::"] 

676 st = StringIO() 

677 traceback.print_exc(file=st) 

678 content.append("") 

679 trace = st.getvalue() 

680 trace += "\n----------------------OPT\n" + str(p) 

681 trace += "\n----------------------EXC\n" + str(e) 

682 trace += "\n----------------------SETT\n" + str(sett) 

683 trace += "\n----------------------ENV\n" + str(env) 

684 trace += "\n----------------------DOCNAME\n" + str(docname) 

685 trace += "\n----------------------CODE\n" 

686 content.extend(" " + _ for _ in trace.split("\n")) 

687 content.append("") 

688 content.append("") 

689 content.extend(" " + _ for _ in tab.split("\n")) 

690 content = "\n".join(content) 

691 pout = nodes.literal_block(content, content) 

692 secout += pout 

693 dt = None 

694 

695 if dt is not None: 

696 for ch in dt.children: 

697 node += ch 

698 

699 # Regular output. 

700 if not p["rst"] or p["showout"]: 

701 text = p["sout2"] if p["rst"] else p["sout"] 

702 secout = node 

703 if 'out' in p['toggle'] or 'both' in p['toggle']: 

704 hide = TITLES[language_code]['hide'] + \ 

705 ' ' + TITLES[language_code]['outl'] 

706 unhide = TITLES[language_code]['unhide'] + \ 

707 ' ' + TITLES[language_code]['outl'] 

708 secout = collapse_node(hide=hide, unhide=unhide, show=False) 

709 node += secout 

710 elif len(text) > 0: 

711 pout2 = nodes.paragraph(text=text) 

712 node += pout2 

713 pout = nodes.literal_block(content, content) 

714 secout += pout 

715 

716 p['runpython'] = node 

717 

718 # classes 

719 node['classes'] += ["runpython"] 

720 ns = [node] 

721 return ns 

722 

723 def modify_script_before_running(self, script): 

724 """ 

725 Takes the script as a string 

726 and returns another string before it is run. 

727 It does not modify what is displayed. 

728 The function can be overwritten by any class 

729 based on this one. 

730 """ 

731 return script 

732 

733 

734def visit_runpython_node(self, node): 

735 """ 

736 What to do when visiting a node @see cl runpython_node 

737 the function should have different behaviour, 

738 depending on the format, or the setup should 

739 specify a different function for each. 

740 """ 

741 pass 

742 

743 

744def depart_runpython_node(self, node): 

745 """ 

746 What to do when leaving a node @see cl runpython_node 

747 the function should have different behaviour, 

748 depending on the format, or the setup should 

749 specify a different function for each. 

750 """ 

751 pass 

752 

753 

754def setup(app): 

755 """ 

756 setup for ``runpython`` (sphinx) 

757 """ 

758 app.add_config_value('out_runpythonlist', [], 'env') 

759 if hasattr(app, "add_mapping"): 

760 app.add_mapping('runpython', runpython_node) 

761 

762 app.add_node(runpython_node, 

763 html=(visit_runpython_node, depart_runpython_node), 

764 epub=(visit_runpython_node, depart_runpython_node), 

765 elatex=(visit_runpython_node, depart_runpython_node), 

766 latex=(visit_runpython_node, depart_runpython_node), 

767 rst=(visit_runpython_node, depart_runpython_node), 

768 md=(visit_runpython_node, depart_runpython_node), 

769 text=(visit_runpython_node, depart_runpython_node)) 

770 

771 app.add_directive('runpython', RunPythonDirective) 

772 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}