Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- 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 ..loghelper.flog import run_cmd
20from ..texthelper.texts_language import TITLES
21from ..pycode.code_helper import remove_extra_spaces_and_pep8
22from .sphinx_collapse_extension import collapse_node
25class RunPythonCompileError(Exception):
26 """
27 exception raised when a piece of code
28 included in the documentation does not compile
29 """
30 pass
33class RunPythonExecutionError(Exception):
34 """
35 Exception raised when a piece of code
36 included in the documentation raises an exception.
37 """
38 pass
41def run_python_script(script, params=None, comment=None, setsysvar=None, process=False,
42 exception=False, warningout=None, chdir=None, context=None,
43 store_in_file=None):
44 """
45 Executes a script :epkg:`python` as a string.
47 @param script python script
48 @param params params to add before the execution
49 @param comment message to add in a exception when the script fails
50 @param setsysvar if not None, add a member to module *sys*,
51 set up this variable to True,
52 if is remove after the execution
53 @param process run the script in a separate process
54 @param exception expects an exception to be raised,
55 fails if it is not, the function returns no output and the
56 error message
57 @param warningout warning to disable (name of warnings)
58 @param chdir change directory before running this script (if not None)
59 @param context if not None, added to the local context
60 @param store_in_file stores the script into this file
61 and calls tells python the source can be found here,
62 that is useful is the script is using module
63 ``inspect`` to retrieve the source which are not
64 stored in memory
65 @return stdout, stderr, context
67 If the execution throws an exception such as
68 ``NameError: name 'math' is not defined`` after importing
69 the module ``math``. It comes from the fact
70 the domain name used by the function
71 `exec <https://docs.python.org/3/library/functions.html#exec>`_
72 contains the declared objects. Example:
74 ::
76 import math
77 def coordonnees_polaires(x,y):
78 rho = math.sqrt(x*x+y*y)
79 theta = math.atan2 (y,x)
80 return rho, theta
81 coordonnees_polaires(1, 1)
83 The code can be modified into:
85 ::
87 def fake_function():
88 import math
89 def coordonnees_polaires(x,y):
90 rho = math.sqrt(x*x+y*y)
91 theta = math.atan2 (y,x)
92 return rho, theta
93 coordonnees_polaires(1, 1)
94 fake_function()
96 Section :ref:`l-image-rst-runpython` explains
97 how to display an image with this directive.
99 .. versionchanged:: 1.9
100 Parameter *store_in_file* was added.
101 """
102 def warning_filter(warningout):
103 if warningout in (None, ''):
104 warnings.simplefilter("always")
105 elif isinstance(warningout, str):
106 li = [_.strip() for _ in warningout.split()]
107 warning_filter(li)
108 elif isinstance(warningout, list):
109 def interpret(s):
110 return eval(s) if isinstance(s, str) else s
111 warns = [interpret(w) for w in warningout]
112 for w in warns:
113 warnings.simplefilter("ignore", w)
114 else:
115 raise ValueError(
116 "Unexpected value for warningout: {0}".format(warningout))
118 if params is None:
119 params = {}
121 if process:
122 if context is not None and len(context) != 0:
123 raise RunPythonExecutionError(
124 "context cannot be used if the script runs in a separate process.")
126 cmd = sys.executable
127 header = ["# coding: utf-8", "import sys"]
128 if setsysvar:
129 header.append("sys.{0} = True".format(setsysvar))
130 add = 0
131 for path in sys.path:
132 if path.endswith("source") or path.endswith("source/") or path.endswith("source\\"):
133 header.append("sys.path.append('{0}')".format(
134 path.replace("\\", "\\\\")))
135 add += 1
136 if add == 0:
137 for path in sys.path:
138 if path.endswith("src") or path.endswith("src/") or path.endswith("src\\"):
139 header.append("sys.path.append('{0}')".format(
140 path.replace("\\", "\\\\")))
141 add += 1
142 if add == 0:
143 # It did not find any path linked to the copy of
144 # the current module in the documentation
145 # it assumes the first path of `sys.path` is part
146 # of the unit test.
147 path = sys.path[0]
148 path = os.path.join(path, "..", "..", "src")
149 if os.path.exists(path):
150 header.append("sys.path.append('{0}')".format(
151 path.replace("\\", "\\\\")))
152 add += 1
153 else:
154 path = sys.path[0]
155 path = os.path.join(path, "src")
156 if os.path.exists(path):
157 header.append("sys.path.append('{0}')".format(
158 path.replace("\\", "\\\\")))
159 add += 1
161 if add == 0:
162 # We do nothing unless the execution failed.
163 exc_path = RunPythonExecutionError(
164 "Unable to find a path to add:\n{0}".format("\n".join(sys.path)))
165 else:
166 exc_path = None
167 header.append('')
168 script = "\n".join(header) + script
170 if store_in_file is not None:
171 with open(store_in_file, "w", encoding="utf-8") as f:
172 f.write(script)
173 script_arg = None
174 cmd += ' ' + store_in_file
175 else:
176 script_arg = script
178 try:
179 out, err = run_cmd(cmd, script_arg, wait=True, change_path=chdir)
180 return out, err, None
181 except Exception as ee:
182 if not exception:
183 message = ("--SCRIPT--\n{0}\n--PARAMS--\n{1}\n--COMMENT--\n"
184 "{2}\n--ERR--\n{3}\n--OUT--\n{4}\n--EXC--\n{5}"
185 "").format(script, params, comment, "",
186 str(ee), ee)
187 if exc_path:
188 message += "\n---EXC--\n{0}".format(exc_path)
189 raise RunPythonExecutionError(message) from ee
190 return str(ee), str(ee), None
191 else:
192 if store_in_file:
193 raise NotImplementedError(
194 "store_in_file is only implemented if process is True.")
195 try:
196 obj = compile(script, "", "exec")
197 except Exception as ec: # pragma: no cover
198 if comment is None:
199 comment = ""
200 if not exception:
201 message = "SCRIPT:\n{0}\nPARAMS\n{1}\nCOMMENT\n{2}".format(
202 script, params, comment)
203 raise RunPythonCompileError(message) from ec
204 return "", "Cannot compile the do to {0}".format(ec), None
206 globs = globals().copy()
207 loc = locals()
208 for k, v in params.items():
209 loc[k] = v
210 loc["__dict__"] = params
211 if context is not None:
212 for k, v in context.items():
213 globs["__runpython__" + k] = v
214 globs['__runpython__script__'] = script
216 if setsysvar is not None:
217 sys.__dict__[setsysvar] = True
219 sout = StringIO()
220 serr = StringIO()
221 with redirect_stdout(sout):
222 with redirect_stderr(sout):
224 with warnings.catch_warnings():
225 warning_filter(warningout)
227 if chdir is not None:
228 current = os.getcwd()
229 os.chdir(chdir)
231 try:
232 exec(obj, globs, loc)
233 except Exception as ee:
234 if chdir is not None:
235 os.chdir(current)
236 if setsysvar is not None:
237 del sys.__dict__[setsysvar]
238 if comment is None:
239 comment = ""
240 gout = sout.getvalue()
241 gerr = serr.getvalue()
243 excs = traceback.format_exc()
244 lines = excs.split("\n")
245 excs = "\n".join(
246 _ for _ in lines if "sphinx_runpython_extension.py" not in _)
248 if not exception:
249 message = ("--SCRIPT--\n{0}\n--PARAMS--\n{1}\n--COMMENT--"
250 "\n{2}\n--ERR--\n{3}\n--OUT--\n{4}\n--EXC--"
251 "\n{5}\n--TRACEBACK--\n{6}").format(
252 script, params, comment, gout, gerr,
253 ee, excs)
254 raise RunPythonExecutionError(message) from ee
255 return (gout + "\n" + gerr), (gerr + "\n" + excs), None
257 if chdir is not None:
258 os.chdir(current)
260 if setsysvar is not None:
261 del sys.__dict__[setsysvar]
263 gout = sout.getvalue()
264 gerr = serr.getvalue()
265 avoid = {"__runpython____WD__",
266 "__runpython____k__", "__runpython____w__"}
267 context = {k[13:]: v for k, v in globs.items() if k.startswith(
268 "__runpython__") and k not in avoid}
269 return gout, gerr, context
272class runpython_node(nodes.Structural, nodes.Element):
274 """
275 Defines *runpython* node.
276 """
277 pass
280class RunPythonDirective(Directive):
282 """
283 Extracts script to run described by ``.. runpython::``
284 and modifies the documentation.
286 .. exref::
287 :title: A python script which generates documentation
289 The following code prints the version of Python
290 on the standard output. It is added to the documentation::
292 .. runpython::
293 :showcode:
295 import sys
296 print("sys.version_info=", str(sys.version_info))
298 If give the following results:
300 .. runpython::
302 import sys
303 print("sys.version_info=", str(sys.version_info))
305 Options *showcode* can be used to display the code.
306 The option *rst* will assume the output is in RST format and must be
307 interpreted. *showout* will complement the RST output with the raw format.
309 The directive has a couple of options:
311 * ``:assert:`` condition to validate at the end of the execution
312 to check it went right
313 * ``:current:`` runs the script in the source file directory
314 * ``:exception:`` the code throws an exception but it is expected. The error is displayed.
315 * ``:indent:<int>`` to indent the output
316 * ``:language:``: changes ``::`` into ``.. code-block:: language``
317 * ``:linenos:`` to show line numbers
318 * ``:nopep8:`` if present, leaves the code as it is and does not apply pep8 by default,
319 see @see fn remove_extra_spaces_and_pep8.
320 * ``:numpy_precision: <precision>``, run ``numpy.set_printoptions(precision=...)``,
321 precision is 3 by default
322 * ``:process:`` run the script in an another process
323 * ``:restore:`` restore the local context stored in :epkg:`sphinx` application
324 by the previous call to *runpython*
325 * ``:rst:`` to interpret the output, otherwise, it is considered as raw text
326 * ``:setsysvar:`` adds a member to *sys* module, the module can act differently based on that information,
327 if the value is left empty, *sys.enable_disabled_documented_pieces_of_code* will be be set up to *True*.
328 * ``:showcode:`` to show the code before its output
329 * ``:showout`` if *:rst:* is set up, this flag adds the raw rst output to check what is happening
330 * ``:sin:<text_for_in>`` which text to display before the code (by default *In*)
331 * ``:sout:<text_for_in>`` which text to display before the output (by default *Out*)
332 * ``:sphinx:`` by default, function `nested_parse_with_titles
333 <https://www.sphinx-doc.org/en/master/extdev/markupapi.html?highlight=nested_parse#parsing-directive-content-as-rest>`_ is
334 used to parse the output of the script, if this option is set to false,
335 `public_doctree <http://code.nabla.net/doc/docutils/api/docutils/core/docutils.core.publish_doctree.html>`_.
336 * ``:store:`` stores the local context in :epkg:`sphinx` application to restore it later
337 by another call to *runpython*
338 * ``:toggle:`` add a button to hide or show the code, it takes the values
339 ``code`` or ``out`` or ``both``. The direction then hides the given section
340 but adds a button to show it.
341 * ``:warningout:`` name of warnings to disable (ex: ``ImportWarning``),
342 separated by spaces
343 * ``:store_in_file:`` the directive store the script in a file,
344 then executes this file (only if ``:process:`` is enabled),
345 this trick is needed when the script to executes relies on
346 function such :epkg:`*py:inspect:getsource` which requires
347 the script to be stored somewhere in order to retrieve it.
349 Option *rst* can be used the following way::
351 .. runpython::
352 :rst:
354 for l in range(0,10):
355 print("**line**", "*" +str(l)+"*")
356 print('')
358 Which displays interpreted :epkg:`RST`:
360 .. runpython::
361 :rst:
363 for l in range(0,10):
364 print("**line**", "*" +str(l)+"*")
365 print('')
367 If the directive produces RST text to be included later in the documentation,
368 it is able to interpret
369 `docutils directives <http://docutils.sourceforge.net/docs/ref/rst/directives.html>`_
370 and `Sphinx directives
371 <https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html>`_
372 with function `nested_parse_with_titles <http://sphinx-doc.org/extdev/
373 markupapi.html?highlight=nested_parse>`_. However, if this text contains
374 titles, it is better to use option ``:sphinx: false``.
375 Unless *process* option is enabled, global variables cannot be used.
376 `sphinx-autorun <https://pypi.org/project/sphinx-autorun/>`_ offers a similar
377 service except it cannot produce compile :epkg:`RST` content,
378 hide the source and a couple of other options.
379 Option *toggle* can hide or unhide the piece of code
380 or/and its output.
381 The directive also adds local variables such as
382 ``__WD__`` which contains the path to the documentation
383 which contains the directive. It is useful to load additional
384 files ``os.path.join(__WD__, ...)``.
386 .. runpython::
387 :toggle: out
388 :showcode:
390 print("Hide or unhide this output.")
392 .. versionchanged:: 1.9
393 Options *store_in_file* was added.
394 """
395 required_arguments = 0
396 optional_arguments = 0
397 final_argument_whitespace = True
398 option_spec = {
399 'indent': directives.unchanged,
400 'showcode': directives.unchanged,
401 'showout': directives.unchanged,
402 'rst': directives.unchanged,
403 'sin': directives.unchanged,
404 'sout': directives.unchanged,
405 'sphinx': directives.unchanged,
406 'sout2': directives.unchanged,
407 'setsysvar': directives.unchanged,
408 'process': directives.unchanged,
409 'exception': directives.unchanged,
410 'nopep8': directives.unchanged,
411 'warningout': directives.unchanged,
412 'toggle': directives.unchanged,
413 'current': directives.unchanged,
414 'assert': directives.unchanged,
415 'language': directives.unchanged,
416 'store': directives.unchanged,
417 'restore': directives.unchanged,
418 'numpy_precision': directives.unchanged,
419 'store_in_file': directives.unchanged,
420 'linenos': directives.unchanged,
421 }
422 has_content = True
423 runpython_class = runpython_node
425 def run(self):
426 """
427 Extracts the information in a dictionary,
428 runs the script.
430 @return a list of nodes
431 """
432 # settings
433 sett = self.state.document.settings
434 language_code = sett.language_code
435 lineno = self.lineno
437 # add the instance to the global settings
438 if hasattr(sett, "out_runpythonlist"):
439 sett.out_runpythonlist.append(self)
441 # env
442 if hasattr(self.state.document.settings, "env"):
443 env = self.state.document.settings.env
444 else:
445 env = None
447 if env is None:
448 docname = "___unknown_docname___"
449 else:
450 docname = env.docname
452 # post
453 bool_set = (True, 1, "True", "1", "true")
454 bool_set_ = (True, 1, "True", "1", "true", '')
455 p = {
456 'showcode': 'showcode' in self.options,
457 'linenos': 'linenos' in self.options,
458 'showout': 'showout' in self.options,
459 'rst': 'rst' in self.options,
460 'sin': self.options.get('sin', TITLES[language_code]["In"]),
461 'sout': self.options.get('sout', TITLES[language_code]["Out"]),
462 'sout2': self.options.get('sout2', TITLES[language_code]["Out2"]),
463 'sphinx': 'sphinx' not in self.options or self.options['sphinx'] in bool_set,
464 'setsysvar': self.options.get('setsysvar', None),
465 'process': 'process' in self.options and self.options['process'] in bool_set_,
466 'exception': 'exception' in self.options and self.options['exception'] in bool_set_,
467 'nopep8': 'nopep8' in self.options and self.options['nopep8'] in bool_set_,
468 'warningout': self.options.get('warningout', '').strip(),
469 'toggle': self.options.get('toggle', '').strip(),
470 'current': 'current' in self.options and self.options['current'] in bool_set_,
471 'assert': self.options.get('assert', '').strip(),
472 'language': self.options.get('language', '').strip(),
473 'store_in_file': self.options.get('store_in_file', None),
474 'numpy_precision': self.options.get('numpy_precision', '3').strip(),
475 'store': 'store' in self.options and self.options['store'] in bool_set_,
476 'restore': 'restore' in self.options and self.options['restore'] in bool_set_,
477 }
479 if p['setsysvar'] is not None and len(p['setsysvar']) == 0:
480 p['setsysvar'] = 'enable_disabled_documented_pieces_of_code'
481 dind = 0 if p['rst'] else 4
482 p['indent'] = int(self.options.get("indent", dind))
484 # run the script
485 name = "run_python_script_{0}".format(id(p))
486 if p['process']:
487 content = ["if True:"]
488 else:
489 content = ["def {0}():".format(name)]
491 if "numpy" in "\n".join(self.content) and p['numpy_precision'] not in (None, 'None', '-', ''):
492 try:
493 import numpy # pylint: disable=W0611
494 prec = int(p['numpy_precision'])
495 content.append(" import numpy")
496 content.append(" numpy.set_printoptions(%d)" % prec)
497 except (ImportError, ValueError):
498 pass
500 content.append(' ## __WD__ ##')
502 if p["restore"]:
503 context = getattr(env, "runpython_context", None)
504 for k in sorted(context):
505 content.append(
506 " {0} = globals()['__runpython__{0}']".format(k))
507 else:
508 context = None
510 modified_content = self.modify_script_before_running(
511 "\n".join(self.content))
513 if p['assert']:
514 footer = []
515 assert_condition = p['assert'].split('\n')
516 for cond in assert_condition:
517 footer.append("if not({0}):".format(cond))
518 footer.append(
519 " raise AssertionError('''Condition '{0}' failed.''')".format(cond))
520 modified_content += "\n\n" + "\n".join(footer)
522 for line in modified_content.split("\n"):
523 content.append(" " + line)
525 if p["store"]:
526 content.append(' for __k__, __v__ in locals().copy().items():')
527 content.append(
528 " globals()['__runpython__' + __k__] = __v__")
530 if not p['process']:
531 content.append("{0}()".format(name))
533 script = "\n".join(content)
534 script_disp = "\n".join(self.content)
535 if not p["nopep8"]:
536 try:
537 script_disp = remove_extra_spaces_and_pep8(
538 script_disp, is_string=True)
539 except Exception as e: # pragma: no cover
540 if '.' in docname:
541 comment = ' File "{0}", line {1}'.format(docname, lineno)
542 else:
543 comment = ' File "{0}.rst", line {1}\n File "{0}.py", line {1}\n'.format(
544 docname, lineno)
545 raise ValueError(
546 "Pep8 issue with\n'{0}'\n---SCRIPT---\n{1}".format(docname, script)) from e
548 # if an exception is raised, the documentation should report a warning
549 # return [document.reporter.warning('messagr', line=self.lineno)]
550 current_source = self.state.document.current_source
551 docstring = ":docstring of " in current_source
552 if docstring:
553 current_source = current_source.split(":docstring of ")[0]
554 if os.path.exists(current_source):
555 comment = ' File "{0}", line {1}'.format(current_source, lineno)
556 if docstring:
557 new_name = os.path.split(current_source)[0] + ".py"
558 comment += '\n File "{0}", line {1}'.format(new_name, lineno)
559 cs_source = current_source
560 else:
561 if '.' in docname:
562 comment = ' File "{0}", line {1}'.format(docname, lineno)
563 else:
564 comment = ' File "{0}.rst", line {1}\n File "{0}.py", line {1}\n'.format(
565 docname, lineno)
566 cs_source = docname
568 # Add __WD__.
569 cs_source_dir = os.path.dirname(cs_source).replace("\\", "/")
570 script = script.replace(
571 '## __WD__ ##', "__WD__ = '{0}'".format(cs_source_dir))
573 out, err, context = run_python_script(script, comment=comment, setsysvar=p['setsysvar'],
574 process=p["process"], exception=p['exception'],
575 warningout=p['warningout'],
576 chdir=cs_source_dir if p['current'] else None,
577 context=context, store_in_file=p['store_in_file'])
579 if p['store']:
580 # Stores modified local context.
581 setattr(env, "runpython_context", context)
582 else:
583 context = {}
584 setattr(env, "runpython_context", context)
586 if out is not None:
587 out = out.rstrip(" \n\r\t")
588 if err is not None:
589 err = err.rstrip(" \n\r\t")
590 content = out
591 if len(err) > 0:
592 content += "\n[runpythonerror]\n" + err
594 # add member
595 self.exe_class = p.copy()
596 self.exe_class.update(dict(out=out, err=err, script=script))
598 # add indent
599 def add_indent(content, nbind):
600 "local function"
601 lines = content.split("\n")
602 if nbind > 0:
603 lines = [(" " * nbind + _) for _ in lines]
604 content = "\n".join(lines)
605 return content
607 content = add_indent(content, p['indent'])
609 # build node
610 node = self.__class__.runpython_class(rawsource=content, indent=p["indent"],
611 showcode=p["showcode"], rst=p["rst"],
612 sin=p["sin"], sout=p["sout"])
614 if p["showcode"]:
615 if 'code' in p['toggle'] or 'both' in p['toggle']:
616 hide = TITLES[language_code]['hide'] + \
617 ' ' + TITLES[language_code]['code']
618 unhide = TITLES[language_code]['unhide'] + \
619 ' ' + TITLES[language_code]['code']
620 secin = collapse_node(hide=hide, unhide=unhide, show=False)
621 node += secin
622 else:
623 secin = node
624 pin = nodes.paragraph(text=p["sin"])
625 if p['language'] in (None, ''):
626 p['language'] = 'python'
627 if p['language']:
628 pcode = nodes.literal_block(
629 script_disp, script_disp, language=p['language'],
630 linenos=p['linenos'])
631 else:
632 pcode = nodes.literal_block(
633 script_disp, script_disp, linenos=p['linenos'])
634 secin += pin
635 secin += pcode
637 elif len(self.options.get('sout', '')) == 0:
638 p["sout"] = ''
639 p["sout2"] = ''
641 # RST output.
642 if p["rst"]:
643 settings_overrides = {}
644 try:
645 sett.output_encoding
646 except KeyError:
647 settings_overrides["output_encoding"] = "unicode"
648 # try:
649 # sett.doctitle_xform
650 # except KeyError:
651 # settings_overrides["doctitle_xform"] = True
652 try:
653 sett.warning_stream
654 except KeyError: # pragma: no cover
655 settings_overrides["warning_stream"] = StringIO()
656 # 'initial_header_level': 2,
658 secout = node
659 if 'out' in p['toggle'] or 'both' in p['toggle']:
660 hide = TITLES[language_code]['hide'] + \
661 ' ' + TITLES[language_code]['outl']
662 unhide = TITLES[language_code]['unhide'] + \
663 ' ' + TITLES[language_code]['outl']
664 secout = collapse_node(hide=hide, unhide=unhide, show=False)
665 node += secout
666 elif len(p["sout"]) > 0:
667 secout += nodes.paragraph(text=p["sout"])
669 try:
670 if p['sphinx']:
671 st = StringList(content.replace("\r", "").split("\n"))
672 nested_parse_with_titles(self.state, st, secout)
673 dt = None
674 else:
675 dt = core.publish_doctree(
676 content, settings=sett,
677 settings_overrides=settings_overrides)
678 except Exception as e: # pragma: no cover
679 tab = content
680 content = ["::"]
681 st = StringIO()
682 traceback.print_exc(file=st)
683 content.append("")
684 trace = st.getvalue()
685 trace += "\n----------------------OPT\n" + str(p)
686 trace += "\n----------------------EXC\n" + str(e)
687 trace += "\n----------------------SETT\n" + str(sett)
688 trace += "\n----------------------ENV\n" + str(env)
689 trace += "\n----------------------DOCNAME\n" + str(docname)
690 trace += "\n----------------------CODE\n"
691 content.extend(" " + _ for _ in trace.split("\n"))
692 content.append("")
693 content.append("")
694 content.extend(" " + _ for _ in tab.split("\n"))
695 content = "\n".join(content)
696 pout = nodes.literal_block(content, content)
697 secout += pout
698 dt = None
700 if dt is not None:
701 for ch in dt.children:
702 node += ch
704 # Regular output.
705 if not p["rst"] or p["showout"]:
706 text = p["sout2"] if p["rst"] else p["sout"]
707 secout = node
708 if 'out' in p['toggle'] or 'both' in p['toggle']:
709 hide = TITLES[language_code]['hide'] + \
710 ' ' + TITLES[language_code]['outl']
711 unhide = TITLES[language_code]['unhide'] + \
712 ' ' + TITLES[language_code]['outl']
713 secout = collapse_node(hide=hide, unhide=unhide, show=False)
714 node += secout
715 elif len(text) > 0:
716 pout2 = nodes.paragraph(text=text)
717 node += pout2
718 pout = nodes.literal_block(content, content)
719 secout += pout
721 p['runpython'] = node
723 # classes
724 node['classes'] += ["runpython"]
725 ns = [node]
726 return ns
728 def modify_script_before_running(self, script):
729 """
730 Takes the script as a string
731 and returns another string before it is run.
732 It does not modify what is displayed.
733 The function can be overwritten by any class
734 based on this one.
735 """
736 return script
739def visit_runpython_node(self, node):
740 """
741 What to do when visiting a node @see cl runpython_node
742 the function should have different behaviour,
743 depending on the format, or the setup should
744 specify a different function for each.
745 """
746 pass
749def depart_runpython_node(self, node):
750 """
751 What to do when leaving a node @see cl runpython_node
752 the function should have different behaviour,
753 depending on the format, or the setup should
754 specify a different function for each.
755 """
756 pass
759def setup(app):
760 """
761 setup for ``runpython`` (sphinx)
762 """
763 app.add_config_value('out_runpythonlist', [], 'env')
764 if hasattr(app, "add_mapping"):
765 app.add_mapping('runpython', runpython_node)
767 app.add_node(runpython_node,
768 html=(visit_runpython_node, depart_runpython_node),
769 epub=(visit_runpython_node, depart_runpython_node),
770 elatex=(visit_runpython_node, depart_runpython_node),
771 latex=(visit_runpython_node, depart_runpython_node),
772 rst=(visit_runpython_node, depart_runpython_node),
773 md=(visit_runpython_node, depart_runpython_node),
774 text=(visit_runpython_node, depart_runpython_node))
776 app.add_directive('runpython', RunPythonDirective)
777 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}