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

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 

13 

14 

15class UrlNotFoundError(Exception): 

16 """ 

17 Raised when a url does not exist. 

18 """ 

19 

20 def __init__(self, url, code): 

21 Exception.__init__( 

22 self, f"Url not found: returned code={code} for '{url}'") 

23 

24 

25class JavascriptScriptError(ValueError): 

26 """ 

27 Raised when the class does not find what it expects. 

28 """ 

29 pass 

30 

31 

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 

43 

44 

45class RenderJSRaw: 

46 """ 

47 Adds :epkg:`javascript` into a noteboook. 

48 """ 

49 

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) 

107 

108 def _copy_local(self, css, libs, local): 

109 """ 

110 If *self.local*, copies javascript dependencies in the local folder. 

111 

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) 

128 

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) 

134 

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 

148 

149 def generate_html(self): 

150 """ 

151 Overloads method 

152 `_ipython_display_ <http://ipython.readthedocs.io/en/stable/config/integrating.html?highlight=Integrating%20>`_. 

153 

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) 

173 

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) 

214 

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:{") 

223 

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 

248 

249 

250class RenderJSObj(RenderJSRaw): 

251 """ 

252 Renders JS using :epkg:`javascript`. 

253 """ 

254 

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) 

270 

271 

272class RenderJS(RenderJSRaw): 

273 """ 

274 Renders :epkg:`javascript`, only outputs :epkg:`HTML`. 

275 """ 

276 

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