Show error/cancel teardowns in the `Context` diagram
The `context_handshake` sequence diagram only showed the graceful `Return` ending - underselling the whole SC pitch (errors + cancels propagate *both* ways across the wire). Add a "... or, instead of a graceful Return" group with the non-graceful endings, - parent-initiated: `ctx.cancel()` (or a parent-side error) -> child relays `Error: ContextCancelled`, - child-initiated: child ships `Error: <raised>` (or `ContextCancelled`). Labelled `Error: ...` since `ContextCancelled` is an exception that rides inside an `Error` wire msg, not its own msg type - keeping the diagram's wire-level labels honest. Re-render the committed `_diagrams/context_handshake.svg` fallback (CI has no `d2` bin) to match. Also cache-bust d2 images in `d2diagrams.py`: sphinx tags css/js with `?v=<hash>` but not images, so an edited diagram kept serving the browser's stale copy at the stable `_images/<stem>.svg` url (cost us a manual hard-refresh). Hash each rendered svg + rewrite only the d2 `<img src>` to `...svg?v=<hash8>` via the `html-page-context` hook - content-addressed, so unchanged diagrams keep their query. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-codedocs_vibed_for_serious
parent
23edf03563
commit
43216c8d4a
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 93 KiB |
|
|
@ -46,8 +46,10 @@ Usage,
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import enum
|
import enum
|
||||||
|
import hashlib
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess as sp
|
import subprocess as sp
|
||||||
|
|
@ -273,6 +275,17 @@ class D2Diagram(SphinxDirective):
|
||||||
)
|
)
|
||||||
if width := self.options.get('width'):
|
if width := self.options.get('width'):
|
||||||
img['width'] = width
|
img['width'] = width
|
||||||
|
# content-hash the rendered svg so the served
|
||||||
|
# `<img src>` carries a `?v=<hash>` cache-buster
|
||||||
|
# (sphinx tags css/js this way but not images);
|
||||||
|
# without it an edited diagram keeps showing the
|
||||||
|
# browser's stale copy at the stable
|
||||||
|
# `_images/<stem>.svg` url on autobuild reload.
|
||||||
|
if out.exists():
|
||||||
|
img['d2_stem'] = out.stem
|
||||||
|
img['d2_cachebust'] = hashlib.sha256(
|
||||||
|
out.read_bytes()
|
||||||
|
).hexdigest()[:8]
|
||||||
fig = nodes.figure()
|
fig = nodes.figure()
|
||||||
fig += img
|
fig += img
|
||||||
classes: list[str] = (
|
classes: list[str] = (
|
||||||
|
|
@ -318,11 +331,50 @@ def _reset_seen(
|
||||||
_seen_outputs.clear()
|
_seen_outputs.clear()
|
||||||
|
|
||||||
|
|
||||||
|
def _cachebust_d2_images(
|
||||||
|
app: Sphinx,
|
||||||
|
pagename: str,
|
||||||
|
templatename: str,
|
||||||
|
context: dict,
|
||||||
|
doctree,
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
Append a ``?v=<content-hash>`` query to every d2
|
||||||
|
diagram ``<img src>`` in the rendered page ``body``
|
||||||
|
so a browser re-fetches an *edited* SVG instead of
|
||||||
|
its stale cached copy at the stable
|
||||||
|
``_images/<stem>.svg`` url (sphinx cache-busts
|
||||||
|
css/js this way, but not images).
|
||||||
|
|
||||||
|
Scoped strictly to d2 images (those carrying the
|
||||||
|
`d2_cachebust` attr set in `D2Diagram.run()`); all
|
||||||
|
other markup is left untouched.
|
||||||
|
|
||||||
|
'''
|
||||||
|
if doctree is None or 'body' not in context:
|
||||||
|
return
|
||||||
|
busts: dict[str, str] = {}
|
||||||
|
for img in doctree.findall(nodes.image):
|
||||||
|
if ver := img.get('d2_cachebust'):
|
||||||
|
busts[img['d2_stem']] = ver
|
||||||
|
if not busts:
|
||||||
|
return
|
||||||
|
body: str = context['body']
|
||||||
|
for stem, ver in busts.items():
|
||||||
|
body = re.sub(
|
||||||
|
r'(src="[^"]*?' + re.escape(stem) + r'\.svg)(")',
|
||||||
|
rf'\1?v={ver}\2',
|
||||||
|
body,
|
||||||
|
)
|
||||||
|
context['body'] = body
|
||||||
|
|
||||||
|
|
||||||
def setup(app: Sphinx) -> dict:
|
def setup(app: Sphinx) -> dict:
|
||||||
app.add_config_value('d2_bin', None, 'env')
|
app.add_config_value('d2_bin', None, 'env')
|
||||||
app.add_config_value('d2_args', [], 'env')
|
app.add_config_value('d2_args', [], 'env')
|
||||||
app.add_directive('d2', D2Diagram)
|
app.add_directive('d2', D2Diagram)
|
||||||
app.connect('builder-inited', _reset_seen)
|
app.connect('builder-inited', _reset_seen)
|
||||||
|
app.connect('html-page-context', _cachebust_d2_images)
|
||||||
return {
|
return {
|
||||||
'version': '0.1.0',
|
'version': '0.1.0',
|
||||||
# NB: run() writes the rendered SVG during the
|
# NB: run() writes the rendered SVG during the
|
||||||
|
|
|
||||||
|
|
@ -19,3 +19,15 @@ parent -> child: "Yield: stream.send()"
|
||||||
child -> parent: "Yield: stream.send()"
|
child -> parent: "Yield: stream.send()"
|
||||||
parent -> child: "Stop: stream closed"
|
parent -> child: "Stop: stream closed"
|
||||||
child -> parent: "Return: fn return value"
|
child -> parent: "Return: fn return value"
|
||||||
|
# the non-graceful endings: errors/cancels always
|
||||||
|
# propagate, both ways (`ContextCancelled` rides in
|
||||||
|
# an `Error` msg, hence the `Error: ...` labels).
|
||||||
|
"... or, instead of a graceful Return": {
|
||||||
|
parent-initiated: {
|
||||||
|
parent -> child: "ctx.cancel() | parent-side error"
|
||||||
|
child -> parent: "Error: ContextCancelled"
|
||||||
|
}
|
||||||
|
child-initiated: {
|
||||||
|
child -> parent: "Error: child raised | ContextCancelled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue