conandeps: colour tree tags in HTML and text
The tree builder now emits typed tags (path, override, missing, ...) and both renderers colour by kind: <span class> in the HTML <pre> block, ANSI in the terminal (path dim, override yellow, missing red). Previously the HTML tree was one escaped string and only MISSING was coloured in text. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYNdaGDrpaqDyT8QtrTQbM
This commit is contained in:
parent
a99ccd5de8
commit
f018b4c8aa
3 changed files with 59 additions and 22 deletions
|
|
@ -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`
|
||||
(`→`/`<br>` 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 (`<span class=…>`)
|
||||
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.
|
||||
|
|
|
|||
67
conandeps.py
67
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(
|
||||
(
|
||||
"override",
|
||||
"overrides %s (%s by %s)"
|
||||
% (ov.old, "explicit" if ov.explicit else "implicit", ov.by)
|
||||
% (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("<p>None detected.</p>")
|
||||
|
||||
w("<h2>Dependency hierarchy</h2><pre>%s</pre>" % 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(
|
||||
'<span class="%s">%s</span>' % (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("<h2>Dependency hierarchy</h2><pre>%s</pre>" % "\n".join(tree_lines))
|
||||
w("</body></html>")
|
||||
return "".join(parts)
|
||||
|
||||
|
|
|
|||
|
|
@ -344,6 +344,7 @@ def test_text_and_html_render(report, tmp_path):
|
|||
|
||||
coloured = conandeps.render_text(report, color=True)
|
||||
assert "\033[1;31mMISSING\033[0m" in coloured and "\033[32mtest\033[0m" in coloured
|
||||
assert "\033[2mlibfoo -> libbar -> libdeep\033[0m" in coloured # tree path tag, dim
|
||||
# Colour codes must not break column alignment: same visible layout.
|
||||
strip = conandeps._ANSI_RE.sub
|
||||
assert strip("", coloured) == text
|
||||
|
|
@ -351,8 +352,10 @@ def test_text_and_html_render(report, tmp_path):
|
|||
page = conandeps.render_html(report)
|
||||
assert page.startswith("<!DOCTYPE html>") and 'class="missing"' in page
|
||||
assert page.count("<table>") == 3 # dependencies, missing binaries, overrides
|
||||
# The HTML tree is the text tree verbatim, paths included.
|
||||
assert "[libfoo -> libbar -> libdeep]" in page
|
||||
# The HTML tree carries the same tags as the text tree, coloured by kind.
|
||||
assert '[<span class="path">libfoo -> libbar -> libdeep</span>]' in page
|
||||
assert '<span class="new">overrides libbar/1.0 (explicit by your conanfile)</span>' in page
|
||||
assert '<span class="missing">MISSING: Debug,RelWithDebInfo</span>' in page
|
||||
assert 'class="explicit"' in page and 'class="implicit"' in page
|
||||
assert "<script" not in page # self-contained, static
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue