Coverage for src/jyquickhelper/jspy/render_nb_js.py: 93%
169 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-02 00:05 +0200
« prev ^ index » next coverage.py v7.2.7, created at 2023-07-02 00:05 +0200
1# -*- coding: utf-8 -*-
2"""
3@file
4@brief Helpers around JSON
5"""
6import uuid
7import os
8import shutil
9import urllib.request as liburl
10import urllib.error as liberror
11import IPython.core.display as ipydisplay
12from IPython.display import display_html, display_javascript
15class UrlNotFoundError(Exception):
16 """
17 Raised when a url does not exist.
18 """
20 def __init__(self, url, code):
21 Exception.__init__(
22 self, f"Url not found: returned code={code} for '{url}'")
25class JavascriptScriptError(ValueError):
26 """
27 Raised when the class does not find what it expects.
28 """
29 pass
32def check_url(url):
33 "Checks urls."
34 try:
35 liburl.urlopen(url) # pylint: disable=R1732
36 return True
37 except liberror.HTTPError as e:
38 raise UrlNotFoundError(url, e.code) from e
39 except liberror.URLError as e:
40 raise UrlNotFoundError(url, e.reason) from e
41 except Exception as e:
42 raise AssertionError(f"Issue with url '{url}'") from e
45class RenderJSRaw:
46 """
47 Adds :epkg:`javascript` into a noteboook.
48 """
50 def __init__(self, script, width="100%", height="100%", divid=None, css=None,
51 libs=None, style=None, only_html=False, div_class=None, check_urls=True,
52 local=False):
53 """
54 @param script (str) script
55 @param width (str) width
56 @param height (str) height
57 @param style (str) style (added in ``<style>...</style>``)
58 @param divid (str|None) id of the div
59 @param css (list) list of css
60 @param libs (list) list of dependencies
61 @param only_html (bool) use only function
62 `display_html <http://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html?
63 highlight=display_html#IPython.display.display_html>`_
64 and not `display_javascript
65 <http://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html?
66 highlight=display_html#IPython.display.display_javascript>`_ to add
67 javascript to the page.
68 @param div_class (str) class of the section ``div`` which will host the results
69 of the javascript
70 @param check_urls (bool) by default, check url exists
71 @param local (bool|False) use local javascript files
72 """
73 self.script = script
74 self.uuid = divid if divid else "M" + \
75 str(uuid.uuid4()).replace("-", "")
76 if style is None:
77 style = ''
78 if width is not None and 'width' not in style:
79 style += f"width:{width};"
80 if height is not None and 'height' not in style:
81 style += f"height:{height};"
82 if not style:
83 style = None
84 else:
85 if width is not None and 'width' not in style:
86 style += f"width:{width};"
87 if height is not None and 'height' not in style:
88 style += f"height:{height};"
89 self.style = style
90 self.only_html = only_html
91 self.div_class = div_class
92 if "__ID__" not in script:
93 raise JavascriptScriptError(
94 f"The sript does not contain any string __ID__. It is replaced by the ID value in script:\n{script}")
95 self.local = local
96 self.css, self.libs = self._copy_local(css, libs, local)
97 if check_urls and not local:
98 if self.css is not None:
99 for c in self.css:
100 check_url(c)
101 if self.libs is not None:
102 for lib in self.libs:
103 if isinstance(lib, dict):
104 check_url(lib['path'])
105 else:
106 check_url(lib)
108 def _copy_local(self, css, libs, local):
109 """
110 If *self.local*, copies javascript dependencies in the local folder.
112 @param css list of css
113 @param libs list of libraries
114 @param local boolean or new location
115 @return tuple (css, libs)
116 """
117 if not self.local:
118 return css, libs
119 to_copy = []
120 if css:
121 to_copy.extend(css)
122 if libs:
123 for js in libs:
124 if isinstance(js, dict):
125 to_copy.append(js['path'])
126 else:
127 to_copy.append(js)
129 for js in to_copy:
130 if not os.path.exists(js):
131 raise FileNotFoundError(f"Unable to find '{js}'")
132 dest = local if isinstance(local, str) else os.getcwd()
133 shutil.copy(js, dest)
135 if css:
136 css = [os.path.split(c)[-1] for c in css]
137 if libs:
138 def proc(d):
139 "proc"
140 if isinstance(d, dict):
141 d = d.copy()
142 d['path'] = os.path.split(d['path'])[-1]
143 return d
144 else:
145 return os.path.split(d)[-1]
146 libs = [proc(c) for c in libs]
147 return css, libs
149 def generate_html(self):
150 """
151 Overloads method
152 `_ipython_display_ <http://ipython.readthedocs.io/en/stable/config/integrating.html?highlight=Integrating%20>`_.
154 @return `HTML <http://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html#IPython.display.HTML>`_ text,
155 `Javascript <http://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html#IPython.display.Javascript>`_ text
156 """
157 if self.style:
158 style = f' style="{self.style}"'
159 else:
160 style = ""
161 if self.div_class:
162 divcl = f' class="{self.div_class}"'
163 else:
164 divcl = ""
165 if self.css:
166 css = "".join(
167 f'<link rel="stylesheet" href="{c}" type="text/css" />' for c in self.css)
168 ht = '<div id="{uuid}-css">{css}<div{divcl} id="{uuid}"{style}></div></div>'.format(
169 uuid=self.uuid, css=css, style=style, divcl=divcl)
170 else:
171 ht = '<div id="{uuid}-cont"><div{divcl} id="{uuid}"{style}></div></div>'.format(
172 uuid=self.uuid, style=style, divcl=divcl)
174 script = self.script.replace("__ID__", self.uuid)
175 if self.libs:
176 names = []
177 paths = []
178 shims = {}
179 args = []
180 exports = []
181 for lib in self.libs:
182 if isinstance(lib, dict):
183 name = lib.get("name", None)
184 if "path" in lib:
185 p = lib["path"]
186 if name is None:
187 name = ".".join((p.split("/")[-1]).split(".")[:-1])
188 path = ".".join(p.split(".")[:-1])
189 paths.append((name, path))
190 else:
191 raise KeyError(
192 f"unable to find 'path' in {lib}")
193 names.append(name)
194 args.append(name)
195 if "exports" in lib:
196 if name not in shims:
197 shims[name] = {}
198 shims[name]["exports"] = lib["exports"]
199 if isinstance(lib["exports"], list):
200 exports.extend(lib["exports"])
201 else:
202 exports.append(lib["exports"])
203 if "deps" in lib:
204 if name not in shims:
205 shims[name] = {}
206 shims[name]["deps"] = lib["deps"]
207 else:
208 names.append(lib)
209 if len(names) == 0:
210 raise ValueError(
211 "names is empty.\nlibs={0}\npaths={1}\nshims={2}\nexports={3}".format(
212 self.libs, paths, shims, exports))
213 require = ",".join(f"'{na}'" for na in names)
215 config = ["require.config({"]
216 if len(paths) > 0:
217 config.append("paths:{")
218 for name, path in paths:
219 config.append(f"'{name}':'{path}',")
220 config.append("},")
221 if len(shims) > 0:
222 config.append("shim:{")
224 def vd(d):
225 "vd"
226 rows = []
227 for k, v in sorted(d.items()):
228 rows.append("'{0}':{1}".format(
229 k, v if isinstance(v, list) else "'{0}'".format(v)))
230 return "{%s}" % ",".join(rows)
231 for k, v in sorted(shims.items()):
232 config.append(f"'{k}':{vd(v)},")
233 config.append("},")
234 config.append("});")
235 if len(config) > 2:
236 prefix = "\n".join(config) + "\n"
237 else:
238 prefix = ""
239 js = prefix + \
240 """\nrequire([%s], function(%s) { %s });\n""" % (
241 require, ",".join(args), script)
242 else:
243 js = script
244 if self.only_html:
245 ht += f"\n<script>\n{js}\n</script>"
246 return ht, None
247 return ht, js
250class RenderJSObj(RenderJSRaw):
251 """
252 Renders JS using :epkg:`javascript`.
253 """
255 def _ipython_display_(self):
256 """
257 overloads method
258 `_ipython_display_ <http://ipython.readthedocs.io/en/stable/config/integrating.html?highlight=Integrating%20>`_.
259 """
260 if 'display' not in dir(ipydisplay):
261 # Weird bug introduced in IPython 8.0.0
262 import IPython.core.display_functions
263 ipydisplay.display = IPython.core.display_functions.display
264 ht, js = self.generate_html()
265 if js is None:
266 display_html(ht, raw=True)
267 else:
268 display_html(ht, raw=True)
269 display_javascript(js, raw=True)
272class RenderJS(RenderJSRaw):
273 """
274 Renders :epkg:`javascript`, only outputs :epkg:`HTML`.
275 """
277 def _repr_html_(self):
278 """
279 Overloads method *_repr_html_*.
280 """
281 ht, js = self.generate_html()
282 if js is not None:
283 ht += f"\n<script>\n{js}\n</script>\n"
284 return ht