# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. "Mozilla l10n compare locales tool" from collections import defaultdict from .utils import Tree class Observer: def __init__(self, quiet=0, filter=None): """Create Observer For quiet=1, skip per-entity missing and obsolete strings, for quiet=2, skip missing and obsolete files. For quiet=3, skip warnings and errors. """ self.summary = defaultdict( lambda: { "errors": 0, "warnings": 0, "missing": 0, "missing_w": 0, "report": 0, "obsolete": 0, "changed": 0, "changed_w": 0, "unchanged": 0, "unchanged_w": 0, "keys": 0, } ) self.details = Tree(list) self.quiet = quiet self.filter = filter self.error = False def _dictify(self, d): plaindict = {} for k, v in d.items(): plaindict[k] = dict(v) return plaindict def toJSON(self): # Don't export file stats, even if we collected them. # Those are not part of the data we use toJSON for. return { "summary": self._dictify(self.summary), "details": self.details.toJSON(), } def updateStats(self, file, stats): # in multi-project scenarios, this file might not be ours, # check that. # Pass in a dummy entity key '' to avoid getting in to # generic file filters. If we have stats for those, # we want to aggregate the counts if self.filter is not None and self.filter(file, entity="") == "ignore": return for category, value in stats.items(): if category == "errors": # updateStats isn't called with `errors`, but make sure # we handle this if that changes self.error = True self.summary[file.locale][category] += value def notify(self, category, file, data): rv = "error" if category in ["missingFile", "obsoleteFile"]: if self.filter is not None: rv = self.filter(file) if rv == "ignore" or self.quiet >= 2: return rv if self.quiet == 0 or category == "missingFile": self.details[file].append({category: rv}) return rv if self.filter is not None: rv = self.filter(file, data) if rv == "ignore": return rv if category in ["missingEntity", "obsoleteEntity"]: if (category == "missingEntity" and self.quiet < 2) or ( category == "obsoleteEntity" and self.quiet < 1 ): self.details[file].append({category: data}) return rv if category == "error": # Set error independently of quiet self.error = True if category in ("error", "warning"): if (category == "error" and self.quiet < 4) or ( category == "warning" and self.quiet < 3 ): self.details[file].append({category: data}) self.summary[file.locale][category + "s"] += 1 return rv class ObserverList(Observer): def __init__(self, quiet=0): super().__init__(quiet=quiet) self.observers = [] def __iter__(self): return iter(self.observers) def append(self, observer): self.observers.append(observer) def notify(self, category, file, data): """Check observer for the found data, and if it's not to ignore, notify stat_observers. """ rvs = {observer.notify(category, file, data) for observer in self.observers} if all(rv == "ignore" for rv in rvs): return "ignore" # our return value doesn't count super().notify(category, file, data) rvs.discard("ignore") if "error" in rvs: return "error" assert len(rvs) == 1 return rvs.pop() def updateStats(self, file, stats): """Check observer for the found data, and if it's not to ignore, notify stat_observers. """ for observer in self.observers: observer.updateStats(file, stats) super().updateStats(file, stats) def serializeDetails(self): def tostr(t): if t[1] == "key": return " " * t[0] + "/".join(t[2]) o = [] indent = " " * (t[0] + 1) for item in t[2]: if "error" in item: o += [indent + "ERROR: " + item["error"]] elif "warning" in item: o += [indent + "WARNING: " + item["warning"]] elif "missingEntity" in item: o += [indent + "+" + item["missingEntity"]] elif "obsoleteEntity" in item: o += [indent + "-" + item["obsoleteEntity"]] elif "missingFile" in item: o.append(indent + "// add and localize this file") elif "obsoleteFile" in item: o.append(indent + "// remove this file") return "\n".join(o) return "\n".join(tostr(c) for c in self.details.getContent()) def serializeSummaries(self): summaries = {loc: [] for loc in self.summary.keys()} for observer in self.observers: for loc, lst in summaries.items(): # Not all locales are on all projects, # default to empty summary lst.append(observer.summary.get(loc, {})) if len(self.observers) > 1: # add ourselves if there's more than one project for loc, lst in summaries.items(): lst.append(self.summary[loc]) keys = ( "errors", "warnings", "missing", "missing_w", "obsolete", "changed", "changed_w", "unchanged", "unchanged_w", "keys", ) leads = [f"{k:12}" for k in keys] out = [] for locale, summaries in sorted(summaries.items()): if locale: out.append(locale + ":") segment = [""] * len(keys) for summary in summaries: for row, key in enumerate(keys): segment[row] += " {:6}".format(summary.get(key) or "") out += [lead + row for lead, row in zip(leads, segment) if row.strip()] total = sum( summaries[-1].get(k, 0) for k in ["changed", "unchanged", "report", "missing"] ) rate = 0 if total: rate = ( ("changed" in summary and summary["changed"] * 100) or 0 ) / total out.append("%d%% of entries changed" % rate) return "\n".join(out) def __str__(self): return "observer"