Coverage for install/scipp/logging.py: 1%

113 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-11-17 01:51 +0000

1# SPDX-License-Identifier: BSD-3-Clause 

2# Copyright (c) 2023 Scipp contributors (https://github.com/scipp) 

3# @author Jan-Lukas Wynen 

4""" 

5Utilities for managing Scipp's logger and log widget. 

6 

7See the https://scipp.github.io/reference/logging.html for an overview. 

8""" 

9 

10import html 

11import logging 

12import time 

13from copy import copy 

14from dataclasses import dataclass 

15from typing import Any 

16 

17from .core import DataArray, Dataset, Variable 

18from .utils import running_in_jupyter 

19from .visualization import make_html 

20from .visualization.resources import load_style 

21 

22 

23def get_logger() -> logging.Logger: 

24 """ 

25 Return the global logger used by Scipp. 

26 """ 

27 return logging.getLogger('scipp') 

28 

29 

30@dataclass 

31class WidgetLogRecord: 

32 """ 

33 Preprocessed data for display in ``LogWidget``. 

34 """ 

35 

36 name: str 

37 levelname: str 

38 time_stamp: str 

39 message: str 

40 

41 

42def make_log_widget(**widget_kwargs): 

43 if not running_in_jupyter(): 

44 raise RuntimeError( 

45 'Cannot create a logging widget because ' 

46 'Python is not running in a Jupyter notebook.' 

47 ) 

48 

49 from ipywidgets import HTML 

50 

51 class LogWidget(HTML): 

52 """ 

53 Widget that displays log messages in a table. 

54 

55 :seealso: :py:class:`scipp.logging.WidgetHandler` 

56 """ 

57 

58 def __init__(self, **kwargs): 

59 super().__init__(**kwargs) 

60 self._rows_str = '' 

61 self._update() 

62 

63 def add_message(self, record: WidgetLogRecord) -> None: 

64 """ 

65 Add a message to the output. 

66 :param record: Log record formatted for the widget. 

67 """ 

68 self._rows_str += self._format_row(record) 

69 self._update() 

70 

71 @staticmethod 

72 def _format_row(record: WidgetLogRecord) -> str: 

73 # The message is assumed to be safe HTML. 

74 # It is WidgetHandler's responsibility to ensure that. 

75 return ( 

76 f'<tr class="sc-log sc-log-{html.escape(record.levelname.lower())}">' 

77 f'<td class="sc-log-time-stamp">[{html.escape(record.time_stamp)}]</td>' 

78 f'<td class="sc-log-level">{html.escape(record.levelname)}</td>' 

79 f'<td class="sc-log-message">{record.message}</td>' 

80 f'<td class="sc-log-name">&lt;{html.escape(record.name)}&gt;</td>' 

81 '</tr>' 

82 ) 

83 

84 def _update(self) -> None: 

85 self.value = ( 

86 '<div class="sc-log">' 

87 f'<table class="sc-log">{self._rows_str}</table>' 

88 '</div>' 

89 ) 

90 

91 def clear(self) -> None: 

92 """ 

93 Remove all output from the widget. 

94 """ 

95 self._rows_str = '' 

96 self._update() 

97 

98 return LogWidget(**widget_kwargs) 

99 

100 

101def get_log_widget(): 

102 """ 

103 Return the log widget used by the Scipp logger. 

104 If multiple widget handlers are installed, only the first one is returned. 

105 If no widget handler is installed, returns ``None``. 

106 """ 

107 handler = get_widget_handler() 

108 if handler is None: 

109 return None 

110 return handler.widget 

111 

112 

113def display_logs() -> None: 

114 """ 

115 Display the log widget associated with the Scipp logger. 

116 Only works in Jupyter notebooks. 

117 """ 

118 widget = get_log_widget() 

119 if widget is None: 

120 raise RuntimeError( 

121 'Cannot display log widgets because no widget handler is installed.' 

122 ) 

123 

124 from IPython.display import display 

125 from ipywidgets import HTML, VBox 

126 

127 display(VBox([HTML(value=load_style()), widget]).add_class('sc-log-wrap')) 

128 

129 

130def clear_log_widget() -> None: 

131 """ 

132 Remove the current output of the log widget of the Scipp logger 

133 if there is one. 

134 """ 

135 widget = get_log_widget() 

136 if widget is None: 

137 return 

138 widget.clear() 

139 

140 

141def _has_html_repr(x: Any) -> bool: 

142 return isinstance(x, DataArray | Dataset | Variable) 

143 

144 

145def _make_html(x) -> str: 

146 return f'<div class="sc-log-html-payload">{make_html(x)}</div>' 

147 

148 

149# This class is used with the log formatter to distinguish between str and repr. 

150# str produces a pattern that can be replaced with HTML and 

151# repr produces a plain string for the argument. 

152class _ReplacementPattern: 

153 _PATTERN = '$__SCIPP_HTML_REPLACEMENT_{:02d}__' 

154 

155 def __init__(self, i: int, arg: Any): 

156 self._i = i 

157 self._arg = arg 

158 

159 def __str__(self): 

160 return self._PATTERN.format(self._i) 

161 

162 def __repr__(self): 

163 return repr(self._arg) 

164 

165 

166def _preprocess_format_args(args) -> tuple[tuple, dict[str, Any]]: 

167 format_args = [] 

168 replacements = {} 

169 for i, arg in enumerate(args): 

170 if _has_html_repr(arg): 

171 tag = _ReplacementPattern(i, arg) 

172 format_args.append(tag) 

173 replacements[str(tag)] = arg 

174 else: 

175 format_args.append(arg) 

176 return tuple(format_args), replacements 

177 

178 

179def _replace_html_repr(message: str, replacements: dict[str, str]) -> str: 

180 # Do separate check `key in message` in order to avoid calling 

181 # _make_html unnecessarily. Linear string searches are likely less 

182 # expensive than HTML formatting. 

183 for key, repl in replacements.items(): 

184 if key in message: 

185 message = message.replace(key, _make_html(repl)) 

186 return message 

187 

188 

189class WidgetHandler(logging.Handler): 

190 """ 

191 Logging handler that sends messages to a ``LogWidget`` 

192 for display in Jupyter notebooks. 

193 

194 Messages are formatted into a ``WidgetLogRecord`` and not into a string. 

195 

196 This handler introduces special formatting for objects with an HTML representation. 

197 If the log message is a single such object, its HTML repr is embedded in the widget. 

198 Strings are formatted to replace %s with the HTML repr and %r with a plain string 

199 repr using ``str(x)`` and ``repr(x)`` is inaccessible. 

200 """ 

201 

202 def __init__(self, level: int, widget): 

203 super().__init__(level) 

204 self.widget = widget 

205 self._rows = [] 

206 

207 def format(self, record: logging.LogRecord) -> WidgetLogRecord: 

208 """ 

209 Format the specified record for consumption by a ``LogWidget``. 

210 """ 

211 message = ( 

212 self._format_html(record) 

213 if _has_html_repr(record.msg) 

214 else self._format_text(record) 

215 ) 

216 return WidgetLogRecord( 

217 name=record.name, 

218 levelname=record.levelname, 

219 time_stamp=time.strftime( 

220 '%Y-%m-%dT%H:%M:%S', time.localtime(record.created) 

221 ), 

222 message=message, 

223 ) 

224 

225 def _format_text(self, record: logging.LogRecord) -> str: 

226 args, replacements = _preprocess_format_args(record.args) 

227 record = copy(record) 

228 record.args = tuple(args) 

229 message = html.escape(super().format(record)) 

230 return _replace_html_repr(message, replacements) 

231 

232 @staticmethod 

233 def _format_html(record: logging.LogRecord) -> str: 

234 if record.args: 

235 raise TypeError('not all arguments converted during string formatting') 

236 return _make_html(record.msg) 

237 

238 def emit(self, record: logging.LogRecord) -> None: 

239 """ 

240 Send the formatted record to the widget. 

241 """ 

242 self.widget.add_message(self.format(record)) 

243 

244 

245def make_widget_handler() -> WidgetHandler: 

246 """ 

247 Create a new widget log handler. 

248 :raises RuntimeError: If Python is not running in Jupyter. 

249 """ 

250 handler = WidgetHandler(level=logging.INFO, widget=make_log_widget()) 

251 return handler 

252 

253 

254def get_widget_handler() -> WidgetHandler | None: 

255 """ 

256 Return the widget handler installed in the Scipp logger. 

257 If multiple widget handlers are installed, only the first one is returned. 

258 If no widget handler is installed, returns ``None``. 

259 """ 

260 try: 

261 return next( # type: ignore[return-value] 

262 filter( 

263 lambda handler: isinstance(handler, WidgetHandler), 

264 get_logger().handlers, 

265 ) 

266 ) 

267 except StopIteration: 

268 return None