Coverage for nilearn/_utils/html_document.py: 30%

114 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-20 10:58 +0200

1"""Handle HTML plotting.""" 

2 

3import warnings 

4import weakref 

5import webbrowser 

6from html import escape 

7from http import HTTPStatus 

8from http.server import BaseHTTPRequestHandler 

9from pathlib import Path 

10from queue import Empty, Queue 

11from socketserver import TCPServer 

12from threading import Thread 

13 

14from nilearn._utils import remove_parameters 

15from nilearn._utils.logger import find_stack_level 

16 

17MAX_IMG_VIEWS_BEFORE_WARNING = 10 

18BROWSER_TIMEOUT_SECONDS = 3.0 

19 

20WIDTH_DEFAULT = 800 

21HEIGHT_DEFAULT = 800 

22 

23 

24def set_max_img_views_before_warning(new_value): 

25 """Set the number of open views which triggers a warning. 

26 

27 If `None` or a negative number, disable the memory warning. 

28 """ 

29 global MAX_IMG_VIEWS_BEFORE_WARNING 

30 MAX_IMG_VIEWS_BEFORE_WARNING = new_value 

31 

32 

33def _open_in_browser(content): 

34 """Open a page in the user's web browser. 

35 

36 This function starts a local server in a separate thread, opens the page 

37 with webbrowser, and shuts down the server once it has served one request. 

38 """ 

39 queue = Queue() 

40 

41 class Handler(BaseHTTPRequestHandler): 

42 def log_message(self, *args): 

43 del args 

44 

45 def do_GET(self): # noqa: N802 

46 if not self.path.endswith("index.html"): 

47 self.send_error(HTTPStatus.NOT_FOUND, "File not found") 

48 return 

49 self.send_response(HTTPStatus.OK) 

50 self.send_header("Content-type", "text/html") 

51 self.send_header("Content-Length", str(len(content))) 

52 self.end_headers() 

53 self.wfile.write(content) 

54 queue.put("done") 

55 

56 server = TCPServer(("", 0), Handler) 

57 _, port = server.server_address 

58 

59 server_thread = Thread(target=server.serve_forever, daemon=True) 

60 server_thread.start() 

61 

62 url = f"http://localhost:{port}/index.html" 

63 webbrowser.open(url) 

64 try: 

65 queue.get(timeout=BROWSER_TIMEOUT_SECONDS) 

66 except Empty: 

67 raise RuntimeError( 

68 "Failed to open nilearn plot or report in a web browser." 

69 ) 

70 server.shutdown() 

71 server_thread.join() 

72 

73 

74class HTMLDocument: 

75 """Embeds a plot in a web page. 

76 

77 If you are running a Jupyter notebook, the plot will be displayed 

78 inline if this object is the output of a cell. 

79 Otherwise, use ``open_in_browser()`` to open it in a web browser (or 

80 ``save_as_html("filename.html")`` to save it as an html file). 

81 

82 Use ``str(document)`` or ``document.html`` to get the content of the 

83 web page, and ``document.get_iframe()`` to have it wrapped in an iframe. 

84 

85 """ 

86 

87 _all_open_html_repr: weakref.WeakSet = weakref.WeakSet() 

88 

89 def __init__(self, html, width=WIDTH_DEFAULT, height=HEIGHT_DEFAULT): 

90 self.html = html 

91 self.width = width 

92 self.height = height 

93 self._temp_file = None 

94 self._check_n_open() 

95 self._temp_file_removing_proc = None 

96 

97 @property 

98 def width(self): 

99 return self._width 

100 

101 @width.setter 

102 def width(self, value): 

103 try: 

104 value = int(value) 

105 except ValueError: 

106 warnings.warn( 

107 f"Invalid width {value=}. " 

108 f"Using default instead {WIDTH_DEFAULT}", 

109 stacklevel=find_stack_level(), 

110 ) 

111 value = WIDTH_DEFAULT 

112 

113 self._width = value 

114 

115 @property 

116 def height(self): 

117 return self._height 

118 

119 @height.setter 

120 def height(self, value): 

121 try: 

122 value = int(value) 

123 except ValueError: 

124 warnings.warn( 

125 f"Invalid height {value=}. " 

126 f"Using default instead {HEIGHT_DEFAULT}", 

127 stacklevel=find_stack_level(), 

128 ) 

129 value = WIDTH_DEFAULT 

130 self._height = value 

131 

132 def _check_n_open(self): 

133 HTMLDocument._all_open_html_repr.add(self) 

134 if MAX_IMG_VIEWS_BEFORE_WARNING is None: 

135 return 

136 if MAX_IMG_VIEWS_BEFORE_WARNING < 0: 

137 return 

138 if ( 

139 len(HTMLDocument._all_open_html_repr) 

140 > MAX_IMG_VIEWS_BEFORE_WARNING - 1 

141 ): 

142 warnings.warn( 

143 "It seems you have created " 

144 f"more than {MAX_IMG_VIEWS_BEFORE_WARNING} " 

145 "nilearn views. As each view uses dozens " 

146 "of megabytes of RAM, you might want to " 

147 "delete some of them.", 

148 stacklevel=find_stack_level(), 

149 ) 

150 

151 def resize(self, width, height): 

152 """Resize the document displayed. 

153 

154 Parameters 

155 ---------- 

156 width : :obj:`int` 

157 New width of the document. 

158 

159 height : :obj:`int` 

160 New height of the document. 

161 

162 """ 

163 self.width = width 

164 self.height = height 

165 return self 

166 

167 def get_iframe(self, width=None, height=None): 

168 """Get the document wrapped in an inline frame. 

169 

170 For inserting in another HTML page of for display in a Jupyter 

171 notebook. 

172 

173 Parameters 

174 ---------- 

175 width : :obj:`int` or ``None``, default=None 

176 Width of the inline frame. 

177 

178 height : :obj:`int` or ``None``, default=None 

179 Height of the inline frame. 

180 

181 Returns 

182 ------- 

183 wrapped : :obj:`str` 

184 Raw HTML code for the inline frame. 

185 

186 """ 

187 if width is None: 

188 width = self.width 

189 if height is None: 

190 height = self.height 

191 escaped = escape(self.html, quote=True) 

192 wrapped = ( 

193 f'<iframe srcdoc="{escaped}" ' 

194 f'width="{width}" height="{height}" ' 

195 'frameBorder="0"></iframe>' 

196 ) 

197 return wrapped 

198 

199 def get_standalone(self): 

200 """Return the plot in an HTML page.""" 

201 return self.html 

202 

203 def _repr_html_(self): 

204 """Return html representation of the plot. 

205 

206 Used by the Jupyter notebook. 

207 

208 Users normally won't call this method explicitly. 

209 

210 See the jupyter documentation: 

211 https://ipython.readthedocs.io/en/stable/config/integrating.html 

212 """ 

213 return self.get_iframe() 

214 

215 def _repr_mimebundle_(self, include=None, exclude=None): 

216 """Return html representation of the plot. 

217 

218 Used by the Jupyter notebook. 

219 

220 Users normally won't call this method explicitly. 

221 

222 See the jupyter documentation: 

223 https://ipython.readthedocs.io/en/stable/config/integrating.html 

224 """ 

225 del include, exclude 

226 return {"text/html": self.get_iframe()} 

227 

228 def __str__(self): 

229 return self.html 

230 

231 def save_as_html(self, file_name): 

232 """Save the plot in an HTML file, that can later be opened \ 

233 in a browser. 

234 

235 Parameters 

236 ---------- 

237 file_name : :obj:`str` 

238 Path to the HTML file used for saving. 

239 

240 """ 

241 with Path(file_name).open("wb") as f: 

242 f.write(self.get_standalone().encode("utf-8")) 

243 

244 @remove_parameters( 

245 removed_params=["temp_file_lifetime"], 

246 reason=( 

247 "this function does not use a temporary file anymore " 

248 "and 'temp_file_lifetime' has no effect." 

249 ), 

250 end_version="0.13.0", 

251 ) 

252 def open_in_browser( 

253 self, 

254 file_name=None, 

255 temp_file_lifetime="deprecated", # noqa: ARG002 

256 ): 

257 """Save the plot to a temporary HTML file and open it in a browser. 

258 

259 Parameters 

260 ---------- 

261 file_name : :obj:`str` or ``None``, default=None 

262 HTML file to use as a temporary file. 

263 

264 temp_file_lifetime : :obj:`float`, default=30 

265 

266 .. deprecated:: 0.10.3 

267 

268 The parameter is kept for backward compatibility and will be 

269 removed in a future version. It has no effect. 

270 """ 

271 if file_name is None: 

272 _open_in_browser(self.get_standalone().encode("utf-8")) 

273 else: 

274 self.save_as_html(file_name) 

275 webbrowser.open(f"file://{file_name}")