Coverage for src/lektor_ng/reporter.py: 84%
293 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-09-05 14:56 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-09-05 14:56 +0000
1import time
2import traceback
3from contextlib import contextmanager
5import click
6from click import style
7from werkzeug.local import LocalProxy, LocalStack
9_reporter_stack = LocalStack()
10_build_buffer_stack = LocalStack()
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)
27class Reporter:
28 _change_callbacks = set()
30 def __init__(self, env, verbosity=0):
31 self.env = env
32 self.verbosity = verbosity
34 self.builder_stack = []
35 self.artifact_stack = []
36 self.source_stack = []
38 def push(self):
39 _reporter_stack.push(self)
41 @staticmethod
42 def pop():
43 _reporter_stack.pop()
45 def __enter__(self):
46 self.push()
47 return self
49 def __exit__(self, exc_type, exc_value, tb):
50 self.pop()
52 @property
53 def builder(self):
54 if self.builder_stack:
55 return self.builder_stack[-1]
56 return None
58 @property
59 def current_artifact(self):
60 if self.artifact_stack:
61 return self.artifact_stack[-1]
62 return None
64 @property
65 def current_source(self):
66 if self.source_stack:
67 return self.source_stack[-1]
68 return None
70 @property
71 def show_build_info(self):
72 return self.verbosity >= 1
74 @property
75 def show_tracebacks(self):
76 return self.verbosity >= 1
78 @property
79 def show_current_artifacts(self):
80 return self.verbosity >= 2
82 @property
83 def show_artifact_internals(self):
84 return self.verbosity >= 3
86 @property
87 def show_source_internals(self):
88 return self.verbosity >= 3
90 @property
91 def show_debug_info(self):
92 return self.verbosity >= 4
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)
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)
113 def start_build(self, activity):
114 pass
116 def finish_build(self, activity, start_time):
117 pass
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()
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)
138 def start_artifact_build(self, is_current):
139 pass
141 def finish_artifact_build(self, start_time):
142 pass
144 def report_failure(self, artifact, exc_info):
145 pass
147 def report_build_all_failure(self, failures):
148 pass
150 def report_dependencies(self, dependencies):
151 for dep in dependencies:
152 self.report_debug_info("dependency", dep[1])
154 def report_dirty_flag(self, value):
155 pass
157 def report_write_source_info(self, info):
158 pass
160 def report_prune_source_info(self, source):
161 pass
163 def report_sub_artifact(self, artifact):
164 pass
166 def report_build_func(self, build_func):
167 pass
169 def report_debug_info(self, key, value):
170 pass
172 def report_generic(self, message):
173 pass
175 def report_pruned_artifact(self, artifact_name):
176 pass
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()
189 def enter_source(self):
190 pass
192 def leave_source(self, start_time):
193 pass
196class NullReporter(Reporter):
197 pass
200class BufferReporter(Reporter):
201 def __init__(self, env, verbosity=0):
202 Reporter.__init__(self, env, verbosity)
203 self.buffer = []
205 def clear(self):
206 self.buffer = []
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)
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
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
229 def _emit(self, _event, **extra):
230 self.buffer.append((_event, extra))
232 def start_build(self, activity):
233 self._emit("start-build", activity=activity)
235 def finish_build(self, activity, start_time):
236 self._emit("finish-build", activity=activity)
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 )
245 def finish_artifact_build(self, start_time):
246 self._emit("finish-artifact-build", artifact=self.current_artifact)
248 def report_build_all_failure(self, failures):
249 self._emit("build-all-failure", failures=failures)
251 def report_failure(self, artifact, exc_info):
252 self._emit("failure", artifact=artifact, exc_info=exc_info)
254 def report_dirty_flag(self, value):
255 self._emit("dirty-flag", artifact=self.current_artifact, value=value)
257 def report_write_source_info(self, info):
258 self._emit("write-source-info", info=info, artifact=self.current_artifact)
260 def report_prune_source_info(self, source):
261 self._emit("prune-source-info", source=source)
263 def report_build_func(self, build_func):
264 self._emit("build-func", func=describe_build_func(build_func))
266 def report_sub_artifact(self, artifact):
267 self._emit("sub-artifact", artifact=artifact)
269 def report_debug_info(self, key, value):
270 self._emit("debug-info", key=key, value=value)
272 def report_generic(self, message):
273 self._emit("generic", message=message)
275 def enter_source(self):
276 self._emit("enter-source", source=self.current_source)
278 def leave_source(self, start_time):
279 self._emit("leave-source", source=self.current_source)
281 def report_pruned_artifact(self, artifact_name):
282 self._emit("pruned-artifact", artifact_name=artifact_name)
285class CliReporter(Reporter):
286 def __init__(self, env, verbosity=0):
287 Reporter.__init__(self, env, verbosity)
288 self.indentation = 0
290 def indent(self):
291 self.indentation += 1
293 def outdent(self):
294 self.indentation -= 1
296 def _write_line(self, text):
297 click.echo(" " * (self.indentation * 2) + text)
299 def _write_kv_info(self, key, value):
300 self._write_line("{}: {}".format(key, style(str(value), fg="yellow")))
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"))
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 )
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}")
327 self.indent()
329 def finish_artifact_build(self, start_time):
330 self.outdent()
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"))
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})")
341 if not self.show_tracebacks:
342 return
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)
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)
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 )
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)
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))
373 def report_sub_artifact(self, artifact):
374 if self.show_artifact_internals:
375 self._write_kv_info("sub artifact", artifact.artifact_name)
377 def report_debug_info(self, key, value):
378 if self.show_debug_info:
379 self._write_kv_info(key, value)
381 def report_generic(self, message):
382 self._write_line(style(str(message), fg="cyan"))
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()
391 def leave_source(self, start_time):
392 if self.show_source_internals:
393 self.outdent()
395 def report_pruned_artifact(self, artifact_name):
396 self._write_line("{} {}".format(style("D", fg="red"), artifact_name))
399null_reporter = NullReporter(None)
402reporter: Reporter
405@LocalProxy
406def reporter():
407 rv = _reporter_stack.top
408 if rv is None:
409 rv = null_reporter
410 return rv