Coverage for src/lektor_ng/reporter.py: 85%

293 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-09-05 15:42 +0000

1import time 

2import traceback 

3from contextlib import contextmanager 

4 

5import click 

6from click import style 

7from werkzeug.local import LocalProxy, LocalStack 

8 

9_reporter_stack = LocalStack() 

10_build_buffer_stack = LocalStack() 

11 

12 

13def describe_build_func(func): 

14 if hasattr(func, "func"): 

15 func = func.func # unwrap functools.partial 

16 try: 

17 qualname = func.__qualname__ 

18 class_name, _, method = qualname.rpartition(".") 

19 if class_name and method == "build_artifact": 

20 # Strip method name from methods of BuildProgram instances 

21 qualname = class_name 

22 return f"{func.__module__}.{qualname}" 

23 except AttributeError: 

24 return repr(func) 

25 

26 

27class Reporter: 

28 _change_callbacks = set() 

29 

30 def __init__(self, env, verbosity=0): 

31 self.env = env 

32 self.verbosity = verbosity 

33 

34 self.builder_stack = [] 

35 self.artifact_stack = [] 

36 self.source_stack = [] 

37 

38 def push(self): 

39 _reporter_stack.push(self) 

40 

41 @staticmethod 

42 def pop(): 

43 _reporter_stack.pop() 

44 

45 def __enter__(self): 

46 self.push() 

47 return self 

48 

49 def __exit__(self, exc_type, exc_value, tb): 

50 self.pop() 

51 

52 @property 

53 def builder(self): 

54 if self.builder_stack: 

55 return self.builder_stack[-1] 

56 return None 

57 

58 @property 

59 def current_artifact(self): 

60 if self.artifact_stack: 

61 return self.artifact_stack[-1] 

62 return None 

63 

64 @property 

65 def current_source(self): 

66 if self.source_stack: 

67 return self.source_stack[-1] 

68 return None 

69 

70 @property 

71 def show_build_info(self): 

72 return self.verbosity >= 1 

73 

74 @property 

75 def show_tracebacks(self): 

76 return self.verbosity >= 1 

77 

78 @property 

79 def show_current_artifacts(self): 

80 return self.verbosity >= 2 

81 

82 @property 

83 def show_artifact_internals(self): 

84 return self.verbosity >= 3 

85 

86 @property 

87 def show_source_internals(self): 

88 return self.verbosity >= 3 

89 

90 @property 

91 def show_debug_info(self): 

92 return self.verbosity >= 4 

93 

94 @contextmanager 

95 def build(self, activity, builder): 

96 now = time.time() 

97 self.builder_stack.append(builder) 

98 self.start_build(activity) 

99 try: 

100 yield 

101 finally: 

102 self.builder_stack.pop() 

103 self.finish_build(activity, now) 

104 

105 @contextmanager 

106 def on_build_change(self, callback): 

107 self._change_callbacks.add(callback) 

108 try: 

109 yield 

110 finally: 

111 self._change_callbacks.discard(callback) 

112 

113 def start_build(self, activity): 

114 pass 

115 

116 def finish_build(self, activity, start_time): 

117 pass 

118 

119 @contextmanager 

120 def build_artifact(self, artifact, build_func, is_current): 

121 now = time.time() 

122 self.artifact_stack.append(artifact) 

123 self.start_artifact_build(is_current) 

124 self.report_build_func(build_func) 

125 try: 

126 yield 

127 finally: 

128 self.report_artifact_built(artifact, is_current) 

129 self.finish_artifact_build(now) 

130 self.artifact_stack.pop() 

131 

132 def report_artifact_built(self, artifact, is_current): 

133 if is_current: 

134 return 

135 for callback in self._change_callbacks: 

136 callback(artifact) 

137 

138 def start_artifact_build(self, is_current): 

139 pass 

140 

141 def finish_artifact_build(self, start_time): 

142 pass 

143 

144 def report_failure(self, artifact, exc_info): 

145 pass 

146 

147 def report_build_all_failure(self, failures): 

148 pass 

149 

150 def report_dependencies(self, dependencies): 

151 for dep in dependencies: 

152 self.report_debug_info("dependency", dep[1]) 

153 

154 def report_dirty_flag(self, value): 

155 pass 

156 

157 def report_write_source_info(self, info): 

158 pass 

159 

160 def report_prune_source_info(self, source): 

161 pass 

162 

163 def report_sub_artifact(self, artifact): 

164 pass 

165 

166 def report_build_func(self, build_func): 

167 pass 

168 

169 def report_debug_info(self, key, value): 

170 pass 

171 

172 def report_generic(self, message): 

173 pass 

174 

175 def report_pruned_artifact(self, artifact_name): 

176 pass 

177 

178 @contextmanager 

179 def process_source(self, source): 

180 now = time.time() 

181 self.source_stack.append(source) 

182 self.enter_source() 

183 try: 

184 yield 

185 finally: 

186 self.leave_source(now) 

187 self.source_stack.pop() 

188 

189 def enter_source(self): 

190 pass 

191 

192 def leave_source(self, start_time): 

193 pass 

194 

195 

196class NullReporter(Reporter): 

197 pass 

198 

199 

200class BufferReporter(Reporter): 

201 def __init__(self, env, verbosity=0): 

202 Reporter.__init__(self, env, verbosity) 

203 self.buffer = [] 

204 

205 def clear(self): 

206 self.buffer = [] 

207 

208 def get_recorded_dependencies(self): 

209 rv = set() 

210 for event, data in self.buffer: 

211 if event == "debug-info" and data["key"] == "dependency": 

212 rv.add(data["value"]) 

213 return sorted(rv) 

214 

215 def get_major_events(self): 

216 rv = [] 

217 for event, data in self.buffer: 

218 if event not in ("debug-info", "dirty-flag", "write-source-info"): 

219 rv.append((event, data)) 

220 return rv 

221 

222 def get_failures(self): 

223 rv = [] 

224 for event, data in self.buffer: 

225 if event == "failure": 

226 rv.append(data) 

227 return rv 

228 

229 def _emit(self, _event, **extra): 

230 self.buffer.append((_event, extra)) 

231 

232 def start_build(self, activity): 

233 self._emit("start-build", activity=activity) 

234 

235 def finish_build(self, activity, start_time): 

236 self._emit("finish-build", activity=activity) 

237 

238 def start_artifact_build(self, is_current): 

239 self._emit( 

240 "start-artifact-build", 

241 artifact=self.current_artifact, 

242 is_current=is_current, 

243 ) 

244 

245 def finish_artifact_build(self, start_time): 

246 self._emit("finish-artifact-build", artifact=self.current_artifact) 

247 

248 def report_build_all_failure(self, failures): 

249 self._emit("build-all-failure", failures=failures) 

250 

251 def report_failure(self, artifact, exc_info): 

252 self._emit("failure", artifact=artifact, exc_info=exc_info) 

253 

254 def report_dirty_flag(self, value): 

255 self._emit("dirty-flag", artifact=self.current_artifact, value=value) 

256 

257 def report_write_source_info(self, info): 

258 self._emit("write-source-info", info=info, artifact=self.current_artifact) 

259 

260 def report_prune_source_info(self, source): 

261 self._emit("prune-source-info", source=source) 

262 

263 def report_build_func(self, build_func): 

264 self._emit("build-func", func=describe_build_func(build_func)) 

265 

266 def report_sub_artifact(self, artifact): 

267 self._emit("sub-artifact", artifact=artifact) 

268 

269 def report_debug_info(self, key, value): 

270 self._emit("debug-info", key=key, value=value) 

271 

272 def report_generic(self, message): 

273 self._emit("generic", message=message) 

274 

275 def enter_source(self): 

276 self._emit("enter-source", source=self.current_source) 

277 

278 def leave_source(self, start_time): 

279 self._emit("leave-source", source=self.current_source) 

280 

281 def report_pruned_artifact(self, artifact_name): 

282 self._emit("pruned-artifact", artifact_name=artifact_name) 

283 

284 

285class CliReporter(Reporter): 

286 def __init__(self, env, verbosity=0): 

287 Reporter.__init__(self, env, verbosity) 

288 self.indentation = 0 

289 

290 def indent(self): 

291 self.indentation += 1 

292 

293 def outdent(self): 

294 self.indentation -= 1 

295 

296 def _write_line(self, text): 

297 click.echo(" " * (self.indentation * 2) + text) 

298 

299 def _write_kv_info(self, key, value): 

300 self._write_line("{}: {}".format(key, style(str(value), fg="yellow"))) 

301 

302 def start_build(self, activity): 

303 self._write_line(style(f"Started {activity}", fg="cyan")) 

304 if not self.show_build_info: 

305 return 

306 self._write_line(style(f" Tree: {self.env.root_path}", fg="cyan")) 

307 self._write_line(style(f" Output path: {self.builder.destination_path}", fg="cyan")) 

308 

309 def finish_build(self, activity, start_time): 

310 self._write_line( 

311 style( 

312 f"Finished {activity} in {time.time() - start_time:.2f} sec", 

313 fg="cyan", 

314 ) 

315 ) 

316 

317 def start_artifact_build(self, is_current): 

318 artifact = self.current_artifact 

319 if is_current: 

320 if not self.show_current_artifacts: 

321 return 

322 sign = click.style("X", fg="cyan") 

323 else: 

324 sign = click.style("U", fg="green") 

325 self._write_line(f"{sign} {artifact.artifact_name}") 

326 

327 self.indent() 

328 

329 def finish_artifact_build(self, start_time): 

330 self.outdent() 

331 

332 def report_build_all_failure(self, failures): 

333 pluralize = "failure" if failures == 1 else "failures" 

334 self._write_line(click.style(f"Error: Build failed with {failures} {pluralize}.", fg="red")) 

335 

336 def report_failure(self, artifact, exc_info): 

337 sign = click.style("E", fg="red") 

338 err = " ".join("".join(traceback.format_exception_only(*exc_info[:2])).splitlines()).strip() 

339 self._write_line(f"{sign} {artifact.artifact_name} ({err})") 

340 

341 if not self.show_tracebacks: 

342 return 

343 

344 tb = traceback.format_exception(*exc_info) 

345 for line in "".join(tb).splitlines(): 

346 if line.startswith("Traceback "): 

347 line = click.style(line, fg="red") 

348 elif line.startswith(" File "): 

349 line = click.style(line, fg="yellow") 

350 elif not line.startswith(" "): 

351 line = click.style(line, fg="red") 

352 self._write_line(" " + line) 

353 

354 def report_dirty_flag(self, value): 

355 if self.show_artifact_internals and (value or self.show_debug_info): 

356 self._write_kv_info("forcing sources dirty", value) 

357 

358 def report_write_source_info(self, info): 

359 if self.show_artifact_internals and self.show_debug_info: 

360 self._write_kv_info( 

361 "writing source info", 

362 "{} [{}]".format(info.title_i18n["en"], info.type), 

363 ) 

364 

365 def report_prune_source_info(self, source): 

366 if self.show_artifact_internals and self.show_debug_info: 

367 self._write_kv_info("pruning source info", source) 

368 

369 def report_build_func(self, build_func): 

370 if self.show_artifact_internals: 

371 self._write_kv_info("build program", describe_build_func(build_func)) 

372 

373 def report_sub_artifact(self, artifact): 

374 if self.show_artifact_internals: 

375 self._write_kv_info("sub artifact", artifact.artifact_name) 

376 

377 def report_debug_info(self, key, value): 

378 if self.show_debug_info: 

379 self._write_kv_info(key, value) 

380 

381 def report_generic(self, message): 

382 self._write_line(style(str(message), fg="cyan")) 

383 

384 def enter_source(self): 

385 if not self.show_source_internals: 

386 return 

387 current_source = style(repr(self.current_source), fg="magenta") 

388 self._write_line(f"Source {current_source}") 

389 self.indent() 

390 

391 def leave_source(self, start_time): 

392 if self.show_source_internals: 

393 self.outdent() 

394 

395 def report_pruned_artifact(self, artifact_name): 

396 self._write_line("{} {}".format(style("D", fg="red"), artifact_name)) 

397 

398 

399null_reporter = NullReporter(None) 

400 

401 

402reporter: Reporter 

403 

404 

405@LocalProxy 

406def reporter(): 

407 rv = _reporter_stack.top 

408 if rv is None: 

409 rv = null_reporter 

410 return rv