1import time
2import traceback
3from contextlib import contextmanager
4
5import click
6from click import style
7from werkzeug.local import LocalProxy
8from werkzeug.local import LocalStack
9
10from lektor._compat import text_type
11
12
13_reporter_stack = LocalStack()
14_build_buffer_stack = LocalStack()
15
16
17def describe_build_func(func):
18    self = getattr(func, "__self__", None)
19    if self is not None and any(
20        x.__name__ == "BuildProgram" for x in self.__class__.__mro__
21    ):
22        return self.__class__.__module__ + "." + self.__class__.__name__
23    return func.__module__ + "." + func.__name__
24
25
26class Reporter(object):
27    def __init__(self, env, verbosity=0):
28        self.env = env
29        self.verbosity = verbosity
30
31        self.builder_stack = []
32        self.artifact_stack = []
33        self.source_stack = []
34
35    def push(self):
36        _reporter_stack.push(self)
37
38    def pop(self):
39        _reporter_stack.pop()
40
41    def __enter__(self):
42        self.push()
43        return self
44
45    def __exit__(self, exc_type, exc_value, tb):
46        self.pop()
47
48    @property
49    def builder(self):
50        if self.builder_stack:
51            return self.builder_stack[-1]
52        return None
53
54    @property
55    def current_artifact(self):
56        if self.artifact_stack:
57            return self.artifact_stack[-1]
58        return None
59
60    @property
61    def current_source(self):
62        if self.source_stack:
63            return self.source_stack[-1]
64        return None
65
66    @property
67    def show_build_info(self):
68        return self.verbosity >= 1
69
70    @property
71    def show_tracebacks(self):
72        return self.verbosity >= 1
73
74    @property
75    def show_current_artifacts(self):
76        return self.verbosity >= 2
77
78    @property
79    def show_artifact_internals(self):
80        return self.verbosity >= 3
81
82    @property
83    def show_source_internals(self):
84        return self.verbosity >= 3
85
86    @property
87    def show_debug_info(self):
88        return self.verbosity >= 4
89
90    @contextmanager
91    def build(self, activity, builder):
92        now = time.time()
93        self.builder_stack.append(builder)
94        self.start_build(activity)
95        try:
96            yield
97        finally:
98            self.builder_stack.pop()
99            self.finish_build(activity, now)
100
101    def start_build(self, activity):
102        pass
103
104    def finish_build(self, activity, start_time):
105        pass
106
107    @contextmanager
108    def build_artifact(self, artifact, build_func, is_current):
109        now = time.time()
110        self.artifact_stack.append(artifact)
111        self.start_artifact_build(is_current)
112        self.report_build_func(build_func)
113        try:
114            yield
115        finally:
116            self.finish_artifact_build(now)
117            self.artifact_stack.pop()
118
119    def start_artifact_build(self, is_current):
120        pass
121
122    def finish_artifact_build(self, start_time):
123        pass
124
125    def report_failure(self, artifact, exc_info):
126        pass
127
128    def report_build_all_failure(self, failures):
129        pass
130
131    def report_dependencies(self, dependencies):
132        for dep in dependencies:
133            self.report_debug_info("dependency", dep[1])
134
135    def report_dirty_flag(self, value):
136        pass
137
138    def report_write_source_info(self, info):
139        pass
140
141    def report_prune_source_info(self, source):
142        pass
143
144    def report_sub_artifact(self, artifact):
145        pass
146
147    def report_build_func(self, build_func):
148        pass
149
150    def report_debug_info(self, key, value):
151        pass
152
153    def report_generic(self, message):
154        pass
155
156    def report_pruned_artifact(self, artifact_name):
157        pass
158
159    @contextmanager
160    def process_source(self, source):
161        now = time.time()
162        self.source_stack.append(source)
163        self.enter_source()
164        try:
165            yield
166        finally:
167            self.leave_source(now)
168            self.source_stack.pop()
169
170    def enter_source(self):
171        pass
172
173    def leave_source(self, start_time):
174        pass
175
176
177class NullReporter(Reporter):
178    pass
179
180
181class BufferReporter(Reporter):
182    def __init__(self, env, verbosity=0):
183        Reporter.__init__(self, env, verbosity)
184        self.buffer = []
185
186    def clear(self):
187        self.buffer = []
188
189    def get_recorded_dependencies(self):
190        rv = set()
191        for event, data in self.buffer:
192            if event == "debug-info" and data["key"] == "dependency":
193                rv.add(data["value"])
194        return sorted(rv)
195
196    def get_major_events(self):
197        rv = []
198        for event, data in self.buffer:
199            if event not in ("debug-info", "dirty-flag", "write-source-info"):
200                rv.append((event, data))
201        return rv
202
203    def get_failures(self):
204        rv = []
205        for event, data in self.buffer:
206            if event == "failure":
207                rv.append(data)
208        return rv
209
210    def _emit(self, _event, **extra):
211        self.buffer.append((_event, extra))
212
213    def start_build(self, activity):
214        self._emit("start-build", activity=activity)
215
216    def finish_build(self, activity, start_time):
217        self._emit("finish-build", activity=activity)
218
219    def start_artifact_build(self, is_current):
220        self._emit(
221            "start-artifact-build",
222            artifact=self.current_artifact,
223            is_current=is_current,
224        )
225
226    def finish_artifact_build(self, start_time):
227        self._emit("finish-artifact-build", artifact=self.current_artifact)
228
229    def report_build_all_failure(self, failures):
230        self._emit("build-all-failure", failures=failures)
231
232    def report_failure(self, artifact, exc_info):
233        self._emit("failure", artifact=artifact, exc_info=exc_info)
234
235    def report_dirty_flag(self, value):
236        self._emit("dirty-flag", artifact=self.current_artifact, value=value)
237
238    def report_write_source_info(self, info):
239        self._emit("write-source-info", info=info, artifact=self.current_artifact)
240
241    def report_prune_source_info(self, source):
242        self._emit("prune-source-info", source=source)
243
244    def report_build_func(self, build_func):
245        self._emit("build-func", func=describe_build_func(build_func))
246
247    def report_sub_artifact(self, artifact):
248        self._emit("sub-artifact", artifact=artifact)
249
250    def report_debug_info(self, key, value):
251        self._emit("debug-info", key=key, value=value)
252
253    def report_generic(self, message):
254        self._emit("generic", message=message)
255
256    def enter_source(self):
257        self._emit("enter-source", source=self.current_source)
258
259    def leave_source(self, start_time):
260        self._emit("leave-source", source=self.current_source)
261
262    def report_pruned_artifact(self, artifact_name):
263        self._emit("pruned-artifact", artifact_name=artifact_name)
264
265
266class CliReporter(Reporter):
267    def __init__(self, env, verbosity=0):
268        Reporter.__init__(self, env, verbosity)
269        self.indentation = 0
270
271    def indent(self):
272        self.indentation += 1
273
274    def outdent(self):
275        self.indentation -= 1
276
277    def _write_line(self, text):
278        click.echo(" " * (self.indentation * 2) + text)
279
280    def _write_kv_info(self, key, value):
281        self._write_line("%s: %s" % (key, style(text_type(value), fg="yellow")))
282
283    def start_build(self, activity):
284        self._write_line(style("Started %s" % activity, fg="cyan"))
285        if not self.show_build_info:
286            return
287        self._write_line(style("  Tree: %s" % self.env.root_path, fg="cyan"))
288        self._write_line(
289            style("  Output path: %s" % self.builder.destination_path, fg="cyan")
290        )
291
292    def finish_build(self, activity, start_time):
293        self._write_line(
294            style(
295                "Finished %s in %.2f sec" % (activity, time.time() - start_time),
296                fg="cyan",
297            )
298        )
299
300    def start_artifact_build(self, is_current):
301        artifact = self.current_artifact
302        if is_current:
303            if not self.show_current_artifacts:
304                return
305            sign = click.style("X", fg="cyan")
306        else:
307            sign = click.style("U", fg="green")
308        self._write_line("%s %s" % (sign, artifact.artifact_name))
309
310        self.indent()
311
312    def finish_artifact_build(self, start_time):
313        self.outdent()
314
315    def report_build_all_failure(self, failures):
316        self._write_line(
317            click.style(
318                "Error: Build failed with %s failure%s."
319                % (failures, failures != 1 and "s" or ""),
320                fg="red",
321            )
322        )
323
324    def report_failure(self, artifact, exc_info):
325        sign = click.style("E", fg="red")
326        err = " ".join(
327            "".join(traceback.format_exception_only(*exc_info[:2])).splitlines()
328        ).strip()
329        self._write_line("%s %s (%s)" % (sign, artifact.artifact_name, err))
330
331        if not self.show_tracebacks:
332            return
333
334        tb = traceback.format_exception(*exc_info)
335        for line in "".join(tb).splitlines():
336            if line.startswith("Traceback "):
337                line = click.style(line, fg="red")
338            elif line.startswith("  File "):
339                line = click.style(line, fg="yellow")
340            elif not line.startswith("    "):
341                line = click.style(line, fg="red")
342            self._write_line("  " + line)
343
344    def report_dirty_flag(self, value):
345        if self.show_artifact_internals and (value or self.show_debug_info):
346            self._write_kv_info("forcing sources dirty", value)
347
348    def report_write_source_info(self, info):
349        if self.show_artifact_internals and self.show_debug_info:
350            self._write_kv_info(
351                "writing source info", "%s [%s]" % (info.title_i18n["en"], info.type)
352            )
353
354    def report_prune_source_info(self, source):
355        if self.show_artifact_internals and self.show_debug_info:
356            self._write_kv_info("pruning source info", source)
357
358    def report_build_func(self, build_func):
359        if self.show_artifact_internals:
360            self._write_kv_info("build program", describe_build_func(build_func))
361
362    def report_sub_artifact(self, artifact):
363        if self.show_artifact_internals:
364            self._write_kv_info("sub artifact", artifact.artifact_name)
365
366    def report_debug_info(self, key, value):
367        if self.show_debug_info:
368            self._write_kv_info(key, value)
369
370    def report_generic(self, message):
371        self._write_line(style(text_type(message), fg="cyan"))
372
373    def enter_source(self):
374        if not self.show_source_internals:
375            return
376        self._write_line("Source %s" % style(repr(self.current_source), fg="magenta"))
377        self.indent()
378
379    def leave_source(self, start_time):
380        if self.show_source_internals:
381            self.outdent()
382
383    def report_pruned_artifact(self, artifact_name):
384        self._write_line("%s %s" % (style("D", fg="red"), artifact_name))
385
386
387null_reporter = NullReporter(None)
388
389
390@LocalProxy
391def reporter():
392    rv = _reporter_stack.top
393    if rv is None:
394        rv = null_reporter
395    return rv
396