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
« 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
26class RunPythonCompileError(Exception):
27 """
28 exception raised when a piece of code
29 included in the documentation does not compile
30 """
31 pass
34class RunPythonExecutionError(Exception):
35 """
36 Exception raised when a piece of code
37 included in the documentation raises an exception.
38 """
39 pass
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.
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
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:
75 ::
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)
84 The code can be modified into:
86 ::
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()
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}")
116 if params is None:
117 params = {}
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.")
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
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
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
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
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
213 if setsysvar is not None:
214 sys.__dict__[setsysvar] = True
216 sout = StringIO()
217 serr = StringIO()
218 with redirect_stdout(sout):
219 with redirect_stderr(sout):
221 with warnings.catch_warnings():
222 warning_filter(warningout)
224 if chdir is not None:
225 current = os.getcwd()
226 os.chdir(chdir)
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()
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 _)
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
254 if chdir is not None:
255 os.chdir(current)
257 if setsysvar is not None:
258 del sys.__dict__[setsysvar]
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
269class runpython_node(nodes.Structural, nodes.Element):
271 """
272 Defines *runpython* node.
273 """
274 pass
277class RunPythonDirective(Directive):
279 """
280 Extracts script to run described by ``.. runpython::``
281 and modifies the documentation.
283 .. exref::
284 :title: A python script which generates documentation
286 The following code prints the version of Python
287 on the standard output. It is added to the documentation::
289 .. runpython::
290 :showcode:
292 import sys
293 print("sys.version_info=", str(sys.version_info))
295 If give the following results:
297 .. runpython::
299 import sys
300 print("sys.version_info=", str(sys.version_info))
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.
306 The directive has a couple of options:
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.
346 Option *rst* can be used the following way::
348 .. runpython::
349 :rst:
351 for l in range(0,10):
352 print("**line**", "*" +str(l)+"*")
353 print('')
355 Which displays interpreted :epkg:`RST`:
357 .. runpython::
358 :rst:
360 for l in range(0,10):
361 print("**line**", "*" +str(l)+"*")
362 print('')
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__, ...)``.
383 .. runpython::
384 :toggle: out
385 :showcode:
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
419 def run(self):
420 """
421 Extracts the information in a dictionary,
422 runs the script.
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
431 # add the instance to the global settings
432 if hasattr(sett, "out_runpythonlist"):
433 sett.out_runpythonlist.append(self)
435 # env
436 if hasattr(self.state.document.settings, "env"):
437 env = self.state.document.settings.env
438 else:
439 env = None
441 if env is None:
442 docname = "___unknown_docname___"
443 else:
444 docname = env.docname
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 }
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))
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}():"]
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
494 content.append(' ## __WD__ ##')
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
504 modified_content = self.modify_script_before_running(
505 "\n".join(self.content))
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)
516 for line in modified_content.split("\n"):
517 content.append(" " + line)
519 if p["store"]:
520 content.append(' for __k__, __v__ in locals().copy().items():')
521 content.append(
522 " globals()['__runpython__' + __k__] = __v__")
524 if not p['process']:
525 content.append(f"{name}()")
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}")
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
563 # Add __WD__.
564 cs_source_dir = os.path.dirname(cs_source).replace("\\", "/")
565 script = script.replace(
566 '## __WD__ ##', f"__WD__ = '{cs_source_dir}'")
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'])
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)
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
589 # add member
590 self.exe_class = p.copy()
591 self.exe_class.update(dict(out=out, err=err, script=script))
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
602 content = add_indent(content, p['indent'])
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"])
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
632 elif len(self.options.get('sout', '')) == 0:
633 p["sout"] = ''
634 p["sout2"] = ''
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,
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"])
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
695 if dt is not None:
696 for ch in dt.children:
697 node += ch
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
716 p['runpython'] = node
718 # classes
719 node['classes'] += ["runpython"]
720 ns = [node]
721 return ns
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
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
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
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)
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))
771 app.add_directive('runpython', RunPythonDirective)
772 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}