修复一个在Sphinx中曾经能正常显示的Graphviz dot图像,但现在无法显示
我接手了一门我已经教了好几年的Python课程。我不时重新整理这门课,以便引用最新的Python文档,当然由于课程文档是用Sphinx构建的,每次把构建环境更新到新版以便让外观在与现代Sphinx生成的文档上保持大致一致时,都会带来一些兼容性上的变动。
课程的一部分讲解Python的别名,以及对浅拷贝要小心的地方,我们用一个 digraph 输入来生成所需的图像。不幸的是,我无法找回原始图像(早前丢失),但基本结构大致是这样的:
x → [ | | ]
/ | \
/ | \
/ | \
↙︎ ↓ ↘︎
[1|2|3] [4|5|6] [7|8|9]
↖︎ ↑ ↗︎
\ | /
\ | /
\ | /
y → [ | | ]
其中竖线和对角线是来自 x 和 y 外部 list 的定向箭头,展示了浅拷贝对通过以下方式生成的嵌套 list 的影响:
x = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
y = list(x)
y = x.copy() # equivalent
y = x[:] # equivalent
用于生成该图像的原始代码,在较早版本的Sphinx中能够工作(我现在使用的是9.0.1版本)的原始代码是:
.. digraph:: alias_3
node [colorscheme=pastel28, fillcolor=1, style=filled];
{ node[shape=box, fillcolor=2];
x; y;
}
{ node[shape=record, fillcolor=3];
list123 [label="1|2|3"];
list456 [label="4|5|6"];
list789 [label="7|8|9"];
listX [label="<1>|<2>|<3>"];
listY [label="<1>|<2>|<3>"];
}
subgraph { rank=same; x -> listX; }
subgraph { rank=same; y -> listY; }
listX:1 -> list123;
listX:2 -> list456;
listX:3 -> list789;
list123 -> listY:1 [dir=back];
list456 -> listY:2 [dir=back];
list789 -> listY:3 [dir=back];
但在Sphinx 9.0.1(如果有必要的话,还包括底层程序 dot 2.44.0)时,会报错,提示与那个 digraph 对应的 dot 代码执行出错,以及 stderr 的输出(它以 bytes 的repr显示,但我为了便于阅读而进行了解码):
警告:相邻节点之间存在一个具有记录形状的扁平边 - 将记录替换为HTML风格的标签
边x -> listX
错误:丢失x listX边
错误:丢失y listY边
我大概明白问题所在(原来,dot 指向同等级的记录时箭头是可以的,但现在它不喜欢了,因为它看起来像是指向整条记录中的单个元素,而不是整个记录,因此被禁止),是的,我大概可以通过把 x 放在最顶端并指向它的列表,而 y 指向底部并向上指向它的列表来实现,但我不想这么做,因为:
- 左侧绑定的实际名称对象看起来比较自然;实际名称应在一个可预测的位置被找到,然后从那里继续跟随引用的迷宫,且
- 从三行到五行会大幅增加纵向图像的尺寸,我在课堂上会把它放大显示在屏幕上;五行的显示就只剩下图像,没有空间来仅用于演示目的在图像和生成代码之间来回引用。
我也就此尝试了很久,甚至(真是)向gemma-4求助,结果gemma-4不断给出新的代码块(在我的默认提示中要求对正确性的置信度,它自称有90-100% 的信心),但都没有一个能真正工作。例如:
- 把它改成
shape=none,并把标签做成类似list123 [label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0"><TD>1</TD><TD>2</TD><TD>3</TD></TABLE>>];的形式,构建时没有错误,但显示的是一个框,只包含list123,而不是三个框分别包含1、2和3。尝试把它引用为list123 [label="<TABLE BORDER='0' CELLBORDER='1' CELLSPACING='0'><TD>1</TD><TD>2</TD><TD>3</TD></TABLE>"];时,单个框成功包含原始文本"<TABLE BORDER='0' CELLBORDER='1' CELLSPACING='0'><TD>1</TD><TD>2</TD><TD>3</TD></TABLE>。 - 保持记录形状,将
listX与listY的元素重命名为"<p1> | <p2> | <p3}"(不太清楚为何要改名以添加p,但也就随它去),然后让subgraph语句变成subgraph { rank=same; -> listX:p1; }(以明确表示我们可以指向整条记录的最左边的值,完全没问题我们不指向整个记录),再次失败,并出现我一开始遇到的同样的扁平边错误。 - 再次尝试
shape=none方案,但这次不是使用外层的<>或"",而是在HTML中直接放入内容且没有外部定界符(在第一个'1'处就因为语法错误而失败)。 - 将
subgraph与边声明分离为:
```none subgraph { rank=same; x; listX; } subgraph { rank=same; y; listY; }
x -> listX [constraint=false]; y -> listY [constraint=false]; ```
这点令人困惑地,有时再次失败,仍会出现同样的“指向记录的扁平边”错误,另一些时候却能工作(似乎有一些奇怪的缓存机制在干扰行为;如果我改动一个完全不同的图,有时会让前一个图渲染,哪怕渲染不正确,在此之前根本不渲染),但不会在 x/y 与各自的 list 之间画出边(并且把 y 放在它的 list 的右侧而不是左侧)。
在现代的Sphinx/dot中,我现在所做的还能实现吗?如果能,该怎么做?是不是存在一个XY问题,其实有更简单的方式,用dot语言紧凑地生成一个有向图,显示名称与列表之间的引用关系?
解决方案
我对Sphinx一无所知,但你使用的Graphviz版本相当旧。可以在这里下载15.0:https://www.graphviz.org/download/。
这是不是你要找的?
所有的 记录 节点都改写为HTML(https://www.graphviz.org/doc/info/shapes.html#html)
(gemma-4 已接近,但缺少 <TR>)
//
// from https://stackoverflow.com/questions/79951943/fixing-a-dot-graphviz-image-that-used-to-work-in-sphinx-but-no-longer-does
//
digraph X {
node [colorscheme=pastel28, fillcolor=1, style=filled];
{ node[shape=box, fillcolor=2];
x; y;
}
{ node[shape=record, fillcolor=3];
list123 [shape=none
label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD>1</TD><TD>2</TD><TD>3</TD></TR>
</TABLE>>];
list456 [shape=none
label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD>4</TD><TD>5</TD><TD>6</TD></TR>
</TABLE>>];
list789 [shape=none
label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD>7</TD><TD>8</TD><TD>9</TD></TR>
</TABLE>>];
listX [shape=none
label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD port="1"> </TD><TD port="2"> </TD><TD port="3"> </TD></TR>
</TABLE>>];
listY [shape=none
label=<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0">
<TR><TD port="1"> </TD><TD port="2"> </TD><TD port="3"> </TD></TR>
</TABLE>>];
}
subgraph { rank=same; x -> listX; }
subgraph { rank=same; y -> listY; }
listX:1 -> list123;
listX:2 -> list456;
listX:3 -> list789;
list123 -> listY:1 [dir=back];
list456 -> listY:2 [dir=back];
list789 -> listY:3 [dir=back];
}
