Source code for fontbakery.reporters.serialize

"""
FontBakery reporters/serialize can report the events of the FontBakery
CheckRunner Protocol to a serializeable document e.g. for usage with `json.dumps`.

Separation of Concerns Disclaimer:
While created specifically for checking fonts and font-families this
module has no domain knowledge about fonts. It can be used for any kind
of (document) checking. Please keep it so. It will be valuable for other
domains as well.
Domain specific knowledge should be encoded only in the Profile (Checks,
Conditions) and MAYBE in *customized* reporters e.g. subclasses.
"""
from fontbakery.status import Status, DEBUG, END, ENDCHECK, SECTIONSUMMARY, START
from fontbakery.reporters import FontbakeryReporter


[docs]class SerializeReporter(FontbakeryReporter): """ usage: >> sr = SerializeReporter(runner=runner, collect_results_by='font') >> sr.run() >> import json >> print(json.dumps(sr.getdoc(), sort_keys=True, indent=4)) """ def __init__(self, loglevels, succinct=None, collect_results_by=None, **kwd): super().__init__(**kwd) self.succinct = succinct self.loglevels = loglevels self._results_by = collect_results_by self._items = {} self._doc = None # used when self._results_by is set # this way we minimize our knowledge of the profile self._max_cluster_by_index = None self._observed_checks = {} @staticmethod def _set_metadata(identity, item): section, check, iterargs = identity # If section is None this is the main doc. # If check is None this is `section` # otherwise this `check` pass
[docs] def omit_loglevel(self, msg) -> bool: """Determine if message is below log level.""" return self.loglevels and (self.loglevels[0] > Status(msg))
def _register(self, event): super()._register(event) status, message, identity = event section, check, iterargs = identity key = self._get_key(identity) # not item == True when item is empty item = self._items.get(key, {}) if not item: self._items[key] = item # init if status in (START, END) and not item: item.update({"result": None, "sections": []}) if self._results_by: # give the consumer a clue that/how the sections # are structured differently. item["clusteredBy"] = self._results_by if status == SECTIONSUMMARY: item.update({"key": key, "result": None, "checks": []}) if check: item.update({"key": key, "result": None, "logs": []}) if self._results_by: if self._results_by == "*check": if check.id not in self._observed_checks: self._observed_checks[check.id] = len(self._observed_checks) index = self._observed_checks[check.id] value = check.id else: index = dict(iterargs).get(self._results_by, None) value = None if self.runner: value = self.runner.get_iterarg(self._results_by, index) if index is not None: if self._max_cluster_by_index is not None: self._max_cluster_by_index = max( index, self._max_cluster_by_index ) else: self._max_cluster_by_index = index item["clustered"] = { "name": self._results_by, # 'index' is None if this check did not require self.results_by "index": index, } if ( value ): # Not set if self.runner was not defined on initialization item["clustered"]["value"] = value self._set_metadata(identity, item) if check: item["description"] = check.description if check.documentation: item["documentation"] = check.documentation if check.rationale: item["rationale"] = check.rationale if check.experimental: item["experimental"] = check.experimental if check.severity: item["severity"] = check.severity if item["key"][2] != (): item["filename"] = self.runner.get_iterarg(*item["key"][2][0]) if status == END: item["result"] = message # is a Counter if status == SECTIONSUMMARY: _, item["result"] = message # message[1] is a Counter if status == ENDCHECK: item["result"] = message.name # is a Status if status >= DEBUG: item["logs"].append( { "status": status.name, "message": f"{message}", "traceback": getattr(message, "traceback", None), } )
[docs] def getdoc(self): if not self._ended: raise Exception("Can't create doc before END status was recevived.") if self._doc is not None: return self._doc doc = self._items[self._get_key((None, None, None))] seen = set() # this puts all in the original order for identity in self._order: key = self._get_key(identity) section, _, _ = identity sectionKey = self._get_key((section, None, None)) sectionDoc = self._items[sectionKey] check = self._items[key] if self._results_by: if not sectionDoc["checks"]: clusterlen = self._max_cluster_by_index + 1 if self._results_by != "*check": # + 1 for rests bucket clusterlen += 1 sectionDoc["checks"] = [[] for _ in range(clusterlen)] index = check["clustered"]["index"] if index is None: # last element collects unclustered index = -1 sectionDoc["checks"][index].append( check ) # pytype: disable=attribute-error else: sectionDoc["checks"].append(check) if sectionKey not in seen: seen.add(sectionKey) doc["sections"].append(sectionDoc) self._doc = doc return doc
[docs] def write(self): import json with open(self.output_file, "w", encoding="utf-8") as fh: json.dump(self.getdoc(), fh, sort_keys=True, indent=4) print(f'A report in JSON format has been saved to "{self.output_file}"')