diff --git a/CLAUDE.md b/CLAUDE.md
index 3ee26d7..4bbb287 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -61,6 +61,11 @@ Users install/upgrade on their own machine with
"all paths". The `via` column renders them one per line as `A -> B`
(`→`/`
` in HTML); `_table()` supports multi-line cells. Rows sort on
(depth, path, host before build, name). The tree section keeps graph order.
+- **Tree tags are typed.** `_tree_rows()` yields `(head, [(kind, text)], tail)`
+ with kinds path/build-require/override/range/missing; `_tree_lines()` (text,
+ ANSI via a `fmt_tag` callback) and the HTML renderer (``)
+ colour by kind from that one source. Add new tree annotations there, never
+ by string-replacing rendered lines.
- **Only the remote counts, and the source is always shown.** Conan says
`Cache` for a locally cached binary without asking the remote, so
`_RemoteIndex` verifies cache hits with `search_packages` on the remote.
diff --git a/conandeps.py b/conandeps.py
index ab6158e..3e16cec 100755
--- a/conandeps.py
+++ b/conandeps.py
@@ -433,51 +433,69 @@ _MARK = {
}
-def _tree_lines(report: Report) -> List[str]:
- """Render the dependency hierarchy as an indented tree, marking overrides and gaps."""
+# A tree line is (text before tags, [(tag kind, tag text)], text after tags).
+# Kinds: path, build-require, override, range, missing. Renderers colour by
+# kind; the words are identical in text and HTML.
+_TreeLine = Tuple[str, List[Tuple[str, str]], str]
+
+
+def _tree_rows(report: Report) -> List[_TreeLine]:
+ """Build the dependency hierarchy as indented rows with typed tags."""
by_name = {}
for dep in report.deps.values():
by_name.setdefault(dep.name, dep) # host first (insertion order), build ok as fallback
# (parent ref, child name) -> Override, so the tree can annotate the exact
# edge whose requirement was rewritten.
edge_overrides = {(o.package, o.old.split("/", 1)[0]): o for o in report.overrides}
- lines: List[str] = []
+ rows: List[_TreeLine] = []
def walk(name: str, parent: str, prefix: str, last: bool, seen: Tuple[str, ...]):
dep = by_name.get(name)
branch = "`-- " if last else "|-- "
if dep is None:
- lines.append(prefix + branch + name + " (not in graph)")
+ rows.append((prefix + branch + name, [], " (not in graph)"))
return
- tags = []
+ tags: List[Tuple[str, str]] = []
if seen: # indirect: spell out this branch's path so indentation need not be counted
- tags.append(" -> ".join(seen + (name,)))
+ tags.append(("path", " -> ".join(seen + (name,))))
if dep.build_require:
- tags.append("build-require")
+ tags.append(("build-require", "build-require"))
ov = edge_overrides.get((parent, name))
if ov:
tags.append(
- "overrides %s (%s by %s)"
- % (ov.old, "explicit" if ov.explicit else "implicit", ov.by)
+ (
+ "override",
+ "overrides %s (%s by %s)"
+ % (ov.old, "explicit" if ov.explicit else "implicit", ov.by),
+ )
)
if dep.ref in report.ranges:
- tags.append("from %s" % report.ranges[dep.ref])
+ tags.append(("range", "from %s" % report.ranges[dep.ref]))
missing = dep.missing()
if missing:
- tags.append("MISSING: " + ",".join(missing))
- line = prefix + branch + dep.ref + (" [" + "; ".join(tags) + "]" if tags else "")
+ tags.append(("missing", "MISSING: " + ",".join(missing)))
if name in seen:
- lines.append(line + " (cycle)")
+ rows.append((prefix + branch + dep.ref, tags, " (cycle)"))
return
- lines.append(line)
+ rows.append((prefix + branch + dep.ref, tags, ""))
child_prefix = prefix + (" " if last else "| ")
for i, child in enumerate(dep.requires):
walk(child, dep.ref, child_prefix, i == len(dep.requires) - 1, seen + (name,))
roots = [d.name for d in report.deps.values() if d.direct]
- lines.append(report.conanfile)
+ rows.append((report.conanfile, [], ""))
for i, name in enumerate(roots):
walk(name, "your conanfile", "", i == len(roots) - 1, ())
+ return rows
+
+
+def _tree_lines(report: Report, fmt_tag=None) -> List[str]:
+ """Flatten tree rows to lines; ``fmt_tag(kind, text)`` decorates each tag."""
+ fmt_tag = fmt_tag or (lambda _kind, text: text)
+ lines = []
+ for head, tags, tail in _tree_rows(report):
+ rendered = " [" + "; ".join(fmt_tag(k, t) for k, t in tags) + "]" if tags else ""
+ lines.append(head + rendered + tail)
return lines
@@ -634,9 +652,8 @@ def render_text(report: Report, color: bool = False) -> str:
# ---- hierarchy -------------------------------------------------------
w(pal.bold("Dependency hierarchy"))
w("")
- for line in _tree_lines(report):
- line = line.replace("MISSING:", pal.bad("MISSING:")) if color else line
- w(line)
+ tag_style = {"path": pal.dim, "override": pal.warn, "missing": pal.bad}
+ out.extend(_tree_lines(report, lambda kind, text: tag_style.get(kind, str)(text)))
return "\n".join(out) + "\n"
@@ -652,11 +669,13 @@ th{background:#eee}code{font-size:.95em}
.missing{color:#a40000;font-weight:700}.missing::before{content:"\\2717 "}
.cacheonly{color:#8a5a00;font-weight:700}.cacheonly::before{content:"\\26A0 "}
.na{color:#777}.old{color:#777;text-decoration:line-through}.new{color:#8a5a00;font-weight:700}
+.path{color:#1b4e8a}
.explicit{color:#1b4e8a;font-weight:700}.implicit{color:#555}
pre{background:#f5f5f5;padding:1rem;overflow-x:auto}
@media (prefers-color-scheme:dark){body{background:#111;color:#eee}th{background:#222}
th,td{border-color:#444}pre{background:#1c1c1c}.ok{color:#6fd66f}.missing{color:#ff7b7b}
-.cacheonly{color:#ffc857}.new{color:#ffc857}.explicit{color:#8ab8f0}.na,.old,.implicit{color:#999}}
+.cacheonly{color:#ffc857}.new{color:#ffc857}.explicit,.path{color:#8ab8f0}
+.na,.old,.implicit{color:#999}}
"""
@@ -736,7 +755,17 @@ def render_html(report: Report) -> str:
else:
w("None detected.
")
- w("Dependency hierarchy
%s
" % e("\n".join(_tree_lines(report))))
+ # Tree text is escaped per fragment so the tag spans survive; the tag
+ # classes reuse the table's colours (path = dim, override = yellow, ...).
+ tag_class = {"path": "path", "override": "new", "missing": "missing", "range": "na"}
+ tree_lines = []
+ for head, tags, tail in _tree_rows(report):
+ spans = "; ".join(
+ '%s' % (tag_class[k], e(t)) if k in tag_class else e(t)
+ for k, t in tags
+ )
+ tree_lines.append(e(head) + (" [" + spans + "]" if tags else "") + e(tail))
+ w("Dependency hierarchy
%s
" % "\n".join(tree_lines))
w("