fix(export): strip control characters from the id drawn for an unlabelled node

A node without a label is drawn under its id (`G.nodes[n].get("label", n)`),
a third path from text to graph.svg. The previous commit cleaned the labels
and left that fallback as it was, so an unlabelled node whose id holds a C0
control character still produced a graph.svg that a strict parser rejects.

Run the fallback through the same strip: one expression,
`_strip_xml_illegal(str(G.nodes[n].get("label", n)))`, now covers the label
and the id. A clean id is drawn as before.

Tests: for each control character the module already lists, an unlabelled
node whose id carries it; the file must parse and still show the cleaned id.

Assisted-by: assayer harness on Claude Code (https://github.com/Yyunozor/assayer-memory-mcp)
This commit is contained in:
Yyunozor committed 2026-10-08 13:46:07 +02:00
1 parent 697c9338b2
commit 2e7e28ab32
2 files changed
+15 -4

No files matched your search

+1 -2
View File
@@ -1393,8 +1393,7 @@ def to_svg(
# svg.fonttype) and escapes only &, <, > and --, so a control character in a label
# would leave the file malformed XML while the export reports success (same hazard as #2897).
nx.draw_networkx_labels(G, pos, ax=ax,
labels={n: (_strip_xml_illegal(str(G.nodes[n]["label"]))
if "label" in G.nodes[n] else n)
labels={n: _strip_xml_illegal(str(G.nodes[n].get("label", n)))
for n in G.nodes()},
font_size=7, font_color="white")
+14 -2
View File
@@ -138,10 +138,10 @@ def test_a_label_that_is_only_control_characters_still_yields_a_name():
# SVG
# ---------------------------------------------------------------------------
def _svg(tmp_path, G, community_labels=None):
def _svg(tmp_path, G, community_labels=None, communities=COMMUNITIES):
pytest.importorskip("matplotlib")
out = tmp_path / "g.svg"
to_svg(G, COMMUNITIES, str(out), community_labels=community_labels)
to_svg(G, communities, str(out), community_labels=community_labels)
return out.read_text(encoding="utf-8")
@@ -179,6 +179,18 @@ def test_svg_clean_labels_are_unchanged(tmp_path):
assert "Ordinary Community (2)" in svg
@pytest.mark.parametrize("ch", BREAK)
def test_svg_survives_a_control_character_in_the_id_of_an_unlabelled_node(tmp_path, ch):
"""A node with no label is drawn under its id: a third path from text to the file."""
G = build_from_json({
"nodes": [{"id": f"we{ch}ird", "file_type": "code", "source_file": "a.py"}],
"edges": [], "hyperedges": [],
})
svg = _svg(tmp_path, G, communities={0: [f"we{ch}ird"]})
ET.fromstring(svg)
assert "weird" in svg
def test_svg_draws_a_label_that_is_not_a_string(tmp_path):
"""nx.draw_networkx_labels str()s a non-string label on its own; the strip
must not turn that into a TypeError, nor into an empty label."""