ooad1-10 Exercise 3: Attribute Drift ============================================ ex3.py (needs uml2py.py, design.mmd and generated_library.py; run from the chapter folder): """Exercise 3: attribute drift. The class diagram's attributes against the fields in the code, after someone edits the generated file by hand. Run from the chapter folder: py ex3/ex3.py""" import ast import pathlib import sys HERE = pathlib.Path(__file__).resolve().parent.parent sys.path.insert(0, str(HERE)) import uml2py # noqa: E402 def code_fields(source: str) -> dict[str, set[str]]: out = {} for node in ast.parse(source).body: if isinstance(node, ast.ClassDef): out[node.name] = {s.target.id for s in node.body if isinstance(s, ast.AnnAssign) and isinstance(s.target, ast.Name)} return out def report(design_text: str, source: str) -> list[str]: design, code = uml2py.parse(design_text), code_fields(source) problems = [] for c in design.values(): if c.kind in ("interface", "abstract", "enumeration"): continue expected = {n for n, _ in c.attrs} | {uml2py.link_name(t, m) for t, m in c.parts + c.refs} actual = code.get(c.name, set()) for name in sorted(expected - actual): problems.append(f"{c.name}.{name}: in the diagram, not in the code") for name in sorted(actual - expected): problems.append(f"{c.name}.{name}: in the code, not in the diagram") return problems design_text = (HERE / "design.mmd").read_text(encoding="utf8") source = (HERE / "generated_library.py").read_text(encoding="utf8") print("1. the generated file, untouched:", report(design_text, source) or "no drift") edited = source.replace(" barcode: str\n", " code: str\n").replace(" status: CopyStatus\n", " status: CopyStatus\n shelf: str = ''\n") print("2. after a hand edit (barcode renamed to code, a shelf field added):") for p in report(design_text, edited): print(" ", p) Run with: py ex3/ex3.py Output: 1. the generated file, untouched: no drift 2. after a hand edit (barcode renamed to code, a shelf field added): Copy.barcode: in the diagram, not in the code Copy.code: in the code, not in the diagram Copy.shelf: in the code, not in the diagram WHY THIS WORKS AS AN ANSWER --------------------------- The untouched file reports no drift, so the check does not cry wolf. After the hand edit it reports three differences: the renamed field appears twice (once missing from the code, once unexpected), which is exactly how a rename looks to a name comparison, and the new field is reported as undrawn. The link fields the generator adds (copies, loans, and so on) are allowed for by computing their names with the same function the generator uses (link_name), so they are not false alarms. Skipped on purpose: interfaces, abstract classes and enumerations, whose fields are not dataclass fields. Like the operations check in the chapter, it compares names, not meaning: a field whose type or purpose changed would pass.