Coverage for pyquickhelper/sphinxext/bokeh/bokeh_plot.py: 72%
96 statements
« prev ^ index » next coverage.py v6.4.2, created at 2022-10-04 04:30 +0200
« prev ^ index » next coverage.py v6.4.2, created at 2022-10-04 04:30 +0200
1"""
2@file
3@brief Modified version of `bokeh_plot.py <https://github.com/bokeh/bokeh/blob/master/bokeh/sphinxext/bokeh_plot.py>`_,
4`LICENSE <https://github.com/bokeh/bokeh/blob/master/LICENSE.txt>`_.
6Include :epkg:`bokeh` plots in Sphinx HTML documentation.
7For other output types, the placeholder text ``[graph]`` will
8be generated.
9The ``bokeh-plot`` directive can be used by either supplying,
10**a path to a source file** as the argument to the directive::
12 .. bokeh-plot:: path/to/plot.py
14.. note::
15 .py scripts are not scanned automatically! In order to include
16 certain directories into .py scanning process use following directive
17 in sphinx conf.py file: bokeh_plot_pyfile_include_dirs = ["dir1","dir2"]
19**Inline code** as the content of the directive::
21 .. bokeh-plot::
23 from bokeh.plotting import figure, output_file, show
25 output_file("example.html")
27 x = [1, 2, 3, 4, 5]
28 y = [6, 7, 6, 4, 5]
30 p = figure(title="example", plot_width=300, plot_height=300)
31 p.line(x, y, line_width=2)
32 p.circle(x, y, size=10, fill_color="white")
34 show(p)
36This directive also works in conjunction with Sphinx autodoc, when
37used in docstrings.
39The ``bokeh-plot`` directive accepts the following options:
41source-position (enum('above', 'below', 'none')):
42 Where to locate the the block of formatted source
43 code (if anywhere).
45linenos (bool):
46 Whether to display line numbers along with the source.
48Examples
49--------
51The inline example code above produces the following output:
53.. bokeh-plot::
55 from bokeh.plotting import figure, output_file, show
57 output_file("example.html")
59 x = [1, 2, 3, 4, 5]
60 y = [6, 7, 6, 4, 5]
62 p = figure(title="example", plot_width=300, plot_height=300)
63 p.line(x, y, line_width=2)
64 p.circle(x, y, size=10, fill_color="white")
66 show(p)
67"""
69# -----------------------------------------------------------------------------
70# Boilerplate
71# -----------------------------------------------------------------------------
72# use the wrapped sphinx logger
73from sphinx.util import logging # isort:skip
74log = logging.getLogger(__name__)
76# -----------------------------------------------------------------------------
77# Imports
78# -----------------------------------------------------------------------------
80# Standard library imports
81from os import getenv
82from os.path import basename, dirname, join
83from uuid import uuid4
85# External imports
86from docutils import nodes
87from docutils.parsers.rst import Directive
88from docutils.parsers.rst.directives import choice, flag
89from sphinx.errors import SphinxError
90from sphinx.util import copyfile, ensuredir, status_iterator
91from sphinx.util.nodes import set_source_info
93# Bokeh imports
94from bokeh.document import Document
95from bokeh.embed import autoload_static
96from bokeh.model import Model
98# Bokeh imports
99from bokeh.sphinxext.example_handler import ExampleHandler
100from bokeh.sphinxext.util import get_sphinx_resources
102# -----------------------------------------------------------------------------
103# Globals and constants
104# -----------------------------------------------------------------------------
106__all__ = (
107 'BokehPlotDirective',
108 'setup',
109)
111# -----------------------------------------------------------------------------
112# General API
113# -----------------------------------------------------------------------------
116class BokehPlotDirective(Directive):
118 has_content = True
119 optional_arguments = 2
121 option_spec = {
122 'source-position': lambda x: choice(x, ('below', 'above', 'none')),
123 'linenos': lambda x: True if flag(x) is None else False,
124 }
126 def run(self):
128 env = self.state.document.settings.env
130 # filename *or* python code content, but not both
131 if self.arguments and self.content:
132 raise SphinxError(
133 "bokeh-plot:: directive can't have both args and content")
135 # need docname not to look like a path
136 docname = env.docname.replace("/", "-")
138 if self.content:
139 log.debug("[bokeh-plot] handling inline example in %r", env.docname)
140 path = env.bokeh_plot_auxdir # code runner just needs any real path
141 source = '\n'.join(self.content)
142 else:
143 try:
144 log.debug("[bokeh-plot] handling external example in %r: %s",
145 env.docname, self.arguments[0])
146 path = self.arguments[0]
147 if not path.startswith("/"):
148 path = join(env.app.srcdir, path)
149 source = open(path).read()
150 except Exception as e: # pragma: no cover
151 raise SphinxError(f"{env.docname}: {e!r}")
153 js_name = f"bokeh-plot-{uuid4().hex}-external-{docname}.js"
155 try:
156 (script, js, js_path, source) = _process_script(
157 source, path, env, js_name)
158 except Exception as e: # pragma: no cover
159 raise RuntimeError(
160 f"Sphinx bokeh-plot exception: \n\n{e}\n\n Failed on:\n\n {source}")
161 env.bokeh_plot_files[js_name] = (
162 script, js, js_path, source, dirname(env.docname))
164 # use the source file name to construct a friendly target_id
165 target_id = f"{env.docname}.{basename(js_path)}"
166 target = nodes.target('', '', ids=[target_id])
167 result = [target]
169 linenos = self.options.get('linenos', False)
170 code = nodes.literal_block(
171 source, source, language="python", linenos=linenos, classes=[])
172 set_source_info(self, code)
174 source_position = self.options.get('source-position', 'below')
176 if source_position == "above":
177 result += [code]
179 result += [nodes.raw('', script, format="html")]
181 if source_position == "below":
182 result += [code]
184 return result
186# -----------------------------------------------------------------------------
187# Dev API
188# -----------------------------------------------------------------------------
191def builder_inited(app):
192 app.env.bokeh_plot_auxdir = join(app.env.doctreedir, 'bokeh_plot')
193 if app.env.srcdir is not None and "IMPOSSIBLE:TOFIND" not in app.env.srcdir:
194 # sphinx/_build/doctrees/bokeh_plot
195 # sphinx/_build/doctrees/bokeh_plot
196 ensuredir(app.env.bokeh_plot_auxdir)
198 if not hasattr(app.env, 'bokeh_plot_files'):
199 app.env.bokeh_plot_files = {}
202def build_finished(app, exception):
203 files = set()
205 # Sphinx 3.0.0
206 for docpath, element in app.env.bokeh_plot_files.items():
207 if len(element) == 4:
208 script, js, js_path, source = element
209 elif len(element) == 5:
210 script, js, js_path, source, docpath = element
211 else:
212 raise ValueError("\n".join([
213 str(type(element)),
214 str(len(element)),
215 str(element),
216 ])) from e
217 files.add((js_path, docpath))
219 files_iter = status_iterator(sorted(files),
220 'copying bokeh-plot files... ',
221 'brown',
222 len(files),
223 app.verbosity,
224 stringify_func=lambda x: basename(x[0]))
226 for (file, docpath) in files_iter:
227 target = join(app.builder.outdir, docpath, basename(file))
228 ensuredir(dirname(target))
229 try:
230 copyfile(file, target)
231 except OSError as e:
232 raise SphinxError(
233 f'cannot copy local file {file!r}, reason: {e}')
236def setup(app):
237 ''' Required Sphinx extension setup function. '''
238 app.add_config_value('bokeh_plot_pyfile_include_dirs', [], 'html')
239 app.add_directive('bokeh-plot', BokehPlotDirective)
240 app.add_config_value('bokeh_missing_google_api_key_ok', True, 'html')
241 app.connect('builder-inited', builder_inited)
242 app.connect('build-finished', build_finished)
244# -----------------------------------------------------------------------------
245# Private API
246# -----------------------------------------------------------------------------
249def _process_script(source, filename, env, js_name, use_relative_paths=False):
250 # Explicitly make sure old extensions are not included until a better
251 # automatic mechanism is available
252 Model._clear_extensions()
254 # quick and dirty way to inject Google API key
255 if "GOOGLE_API_KEY" in source: # pragma: no cover
256 GOOGLE_API_KEY = getenv('GOOGLE_API_KEY')
257 if GOOGLE_API_KEY is None:
258 if env.config.bokeh_missing_google_api_key_ok:
259 GOOGLE_API_KEY = "MISSING_API_KEY"
260 else:
261 raise SphinxError("The GOOGLE_API_KEY environment variable is not set. Set GOOGLE_API_KEY to a valid API key, "
262 "or set bokeh_missing_google_api_key_ok=True in conf.py to build anyway (with broken GMaps)")
263 run_source = source.replace("GOOGLE_API_KEY", GOOGLE_API_KEY)
264 else:
265 run_source = source
267 c = ExampleHandler(source=run_source, filename=filename)
268 d = Document()
269 c.modify_document(d)
270 if c.error:
271 raise RuntimeError(c.error_detail) # pragma: no cover
273 resources = get_sphinx_resources()
274 js_path = join(env.bokeh_plot_auxdir, js_name)
275 js, script = autoload_static(d.roots[0], resources, js_name)
277 if "IMPOSSIBLE:TOFIND" not in js_path:
278 with open(js_path, "w") as f:
279 f.write(js)
281 return (script, js, js_path, source)
283# -----------------------------------------------------------------------------
284# Code
285# -----------------------------------------------------------------------------