"""Human-readable rendering of a snapshot diff (see diff.compute_diff).""" from __future__ import annotations from typing import Any, Callable def _counts(block: dict[str, Any]) -> str: return "+{0} -{1} ~{2}".format( len(block["added"]), len(block["removed"]), len(block["changed"])) def _fmt_changes(changes: dict[str, list]) -> str: return "; ".join("{0}: {1} -> {2}".format(f, v[0], v[1]) for f, v in sorted(changes.items())) def _group_by(items: list[dict], owner_of: Callable[[dict], str]) -> dict[str, list[dict]]: out: dict[str, list[dict]] = {} for it in items: out.setdefault(owner_of(it) or "?", []).append(it) return out # --- per-axis item formatting ------------------------------------------------------------------ def _fmt_type(t: dict) -> str: tag = " [via module iface]" if t.get("via_module_iface") else "" size = t.get("object_size") cls = t.get("cpp_class") or "?" return "{0} -> {1} (size {2}){3}".format(t["script_name"], cls, size, tag) def _fmt_method(m: dict) -> str: return "{0} (id {1})".format(m["name"], m.get("id")) def _fmt_event(e: dict) -> str: return "{0} (#{1})".format(e["name"], e.get("order")) def _fmt_field(f: dict) -> str: return "{0}: {1}".format(f["name"], f.get("type")) def _fmt_layout(x: dict) -> str: vt = " vtable" if x.get("is_vtable") else "" return "@{0:#x} size {1}{2}".format(x["offset"], x.get("size"), vt) # --- section renderers ------------------------------------------------------------------------- def _section_flat(out: list[str], title: str, block: dict, fmt: Callable[[dict], str], name_of: Callable[[dict], str]) -> None: out.append("") out.append("{0:<16} {1}".format(title, _counts(block))) for it in sorted(block["added"], key=name_of): out.append(" + {0}".format(fmt(it))) for it in sorted(block["removed"], key=name_of): out.append(" - {0}".format(name_of(it))) for ch in sorted(block["changed"], key=lambda c: name_of(c["item"])): out.append(" ~ {0:<22} {1}".format(name_of(ch["item"]), _fmt_changes(ch["changes"]))) def _section_owned(out: list[str], title: str, block: dict, fmt: Callable[[dict], str], owner_of: Callable[[dict], str], name_of: Callable[[dict], str]) -> None: out.append("") out.append("{0:<16} {1}".format(title, _counts(block))) added = _group_by(block["added"], owner_of) removed = _group_by(block["removed"], owner_of) changed = _group_by([c["item"] for c in block["changed"]], owner_of) change_by_id = {id(c["item"]): c for c in block["changed"]} for owner in sorted(set(added) | set(removed) | set(changed)): out.append(" {0}".format(owner)) for it in sorted(added.get(owner, []), key=name_of): out.append(" + {0}".format(fmt(it))) for it in sorted(removed.get(owner, []), key=name_of): out.append(" - {0}".format(name_of(it))) for it in sorted(changed.get(owner, []), key=name_of): out.append(" ~ {0:<22} {1}".format( name_of(it), _fmt_changes(change_by_id[id(it)]["changes"]))) def _dispatch_name(r: dict) -> str: return r.get("name") or "id {0}".format(r.get("id")) def _leaves(items: list, cap: int = 4) -> str: shown = items[:cap] extra = "…+{0}".format(len(items) - cap) if len(items) > cap else "" return ", ".join(shown) + extra def _section_dispatch(out: list[str], block: dict) -> None: """Method bodies (per owner+id), normalised. Each changed entry shows a similarity score and the leaf-level delta (which calls appeared/vanished); a summary line gives the overall drift.""" out.append("") summ = block.get("summary") head = "METHOD BODIES" if summ: head = "{0} (shared {1}, ~{2} changed, mean {3}% similar)".format( "METHOD BODIES", summ["shared"], summ["changed"], summ["mean_similarity"]) out.append("{0}".format(head)) out.append("{0:<16} {1}".format("", _counts(block))) owner_of = lambda r: r["owner"] added = _group_by(block["added"], owner_of) removed = _group_by(block["removed"], owner_of) changed = _group_by([c["item"] for c in block["changed"]], owner_of) change_by_id = {id(c["item"]): c for c in block["changed"]} for owner in sorted(set(added) | set(removed) | set(changed)): out.append(" {0}".format(owner)) for it in sorted(added.get(owner, []), key=_dispatch_name): out.append(" + {0}".format(_dispatch_name(it))) for it in sorted(removed.get(owner, []), key=_dispatch_name): out.append(" - {0}".format(_dispatch_name(it))) for it in sorted(changed.get(owner, []), key=_dispatch_name): ch = change_by_id[id(it)] body = ch.get("body", {}) sim = body.get("similarity") bits = [] if body.get("added"): bits.append("+[{0}]".format(_leaves(body["added"]))) if body.get("removed"): bits.append("-[{0}]".format(_leaves(body["removed"]))) if not bits and "impl" in ch["changes"]: bits.append("impl {0} -> {1}".format(*ch["changes"]["impl"])) label = "{0} {1}%".format(_dispatch_name(it), sim) if sim is not None else _dispatch_name(it) out.append(" ~ {0:<26} {1}".format(label, " ".join(bits))) _EMPTY = {"added": [], "removed": [], "changed": []} def _is_empty(block: dict) -> bool: return not (block["added"] or block["removed"] or block["changed"]) def render_text(diff: dict[str, Any], only: set[str] | None = None) -> str: b = diff["binary"] out: list[str] = ["Engine surface diff"] out.append(" from: {0} [{1}/{2}]".format( b["from"].get("name", "?"), b["from"].get("engine", "?"), b["from"].get("compiler", "?"))) out.append(" to: {0} [{1}/{2}]".format( b["to"].get("name", "?"), b["to"].get("engine", "?"), b["to"].get("compiler", "?"))) def want(axis: str) -> bool: return only is None or axis in only if want("types") and not _is_empty(diff["types"]): _section_flat(out, "TYPES", diff["types"], _fmt_type, lambda t: t["script_name"]) if want("methods") and not _is_empty(diff["methods"]): _section_owned(out, "METHODS", diff["methods"], _fmt_method, lambda m: m["owner"], lambda m: m["name"]) if want("events") and not _is_empty(diff["events"]): _section_owned(out, "EVENTS", diff["events"], _fmt_event, lambda e: e["owner"], lambda e: e["name"]) if want("fields") and not _is_empty(diff["fields"]): _section_owned(out, "FIELDS", diff["fields"], _fmt_field, lambda f: f["owner"], lambda f: f["name"]) if want("layout") and not _is_empty(diff["struct_layout"]): _section_owned(out, "STRUCT LAYOUT", diff["struct_layout"], _fmt_layout, lambda x: x["owner"], lambda x: "@{0:#x}".format(x["offset"])) if want("dispatch") and not _is_empty(diff.get("method_dispatch", _EMPTY)): _section_dispatch(out, diff["method_dispatch"]) if want("methods") and diff["moved_methods"]: out.append("") out.append("MOVED METHODS {0}".format(len(diff["moved_methods"]))) for m in sorted(diff["moved_methods"], key=lambda x: x["name"]): out.append(" {0}: {1} -> {2}".format( m["name"], ",".join(m["from_owners"]), ",".join(m["to_owners"]))) if all(_is_empty(diff.get(a, _EMPTY)) for a in ("types", "methods", "events", "fields", "struct_layout", "method_dispatch")): out.append("") out.append("(no differences)") return "\n".join(out)