mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Enable automatic formatting for `sphinx/ext/graphviz.py
`
This commit is contained in:
parent
13d25d96bf
commit
ff8bb72aba
@ -415,7 +415,6 @@ exclude = [
|
|||||||
"sphinx/domains/python/_object.py",
|
"sphinx/domains/python/_object.py",
|
||||||
"sphinx/domains/rst.py",
|
"sphinx/domains/rst.py",
|
||||||
"sphinx/domains/std/__init__.py",
|
"sphinx/domains/std/__init__.py",
|
||||||
"sphinx/ext/graphviz.py",
|
|
||||||
"sphinx/ext/ifconfig.py",
|
"sphinx/ext/ifconfig.py",
|
||||||
"sphinx/ext/imgconverter.py",
|
"sphinx/ext/imgconverter.py",
|
||||||
"sphinx/ext/imgmath.py",
|
"sphinx/ext/imgmath.py",
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
"""Allow graphviz-formatted graphs to be included inline in generated documents.
|
"""Allow graphviz-formatted graphs to be included inline in generated documents."""
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@ -62,14 +61,15 @@ class ClickableMapDefinition:
|
|||||||
def parse(self, dot: str) -> None:
|
def parse(self, dot: str) -> None:
|
||||||
matched = self.maptag_re.match(self.content[0])
|
matched = self.maptag_re.match(self.content[0])
|
||||||
if not matched:
|
if not matched:
|
||||||
raise GraphvizError('Invalid clickable map file found: %s' % self.filename)
|
msg = f'Invalid clickable map file found: {self.filename}'
|
||||||
|
raise GraphvizError(msg)
|
||||||
|
|
||||||
self.id = matched.group(1)
|
self.id = matched.group(1)
|
||||||
if self.id == '%3':
|
if self.id == '%3':
|
||||||
# graphviz generates wrong ID if graph name not specified
|
# graphviz generates wrong ID if graph name not specified
|
||||||
# https://gitlab.com/graphviz/graphviz/issues/1327
|
# https://gitlab.com/graphviz/graphviz/issues/1327
|
||||||
hashed = sha1(dot.encode(), usedforsecurity=False).hexdigest()
|
hashed = sha1(dot.encode(), usedforsecurity=False).hexdigest()
|
||||||
self.id = 'grapviz%s' % hashed[-10:]
|
self.id = f'grapviz{hashed[-10:]}'
|
||||||
self.content[0] = self.content[0].replace('%3', self.id)
|
self.content[0] = self.content[0].replace('%3', self.id)
|
||||||
|
|
||||||
for line in self.content:
|
for line in self.content:
|
||||||
@ -91,7 +91,9 @@ class graphviz(nodes.General, nodes.Inline, nodes.Element):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def figure_wrapper(directive: SphinxDirective, node: graphviz, caption: str) -> nodes.figure:
|
def figure_wrapper(
|
||||||
|
directive: SphinxDirective, node: graphviz, caption: str
|
||||||
|
) -> nodes.figure:
|
||||||
figure_node = nodes.figure('', node)
|
figure_node = nodes.figure('', node)
|
||||||
if 'align' in node:
|
if 'align' in node:
|
||||||
figure_node['align'] = node.attributes.pop('align')
|
figure_node['align'] = node.attributes.pop('align')
|
||||||
@ -131,9 +133,15 @@ class Graphviz(SphinxDirective):
|
|||||||
if self.arguments:
|
if self.arguments:
|
||||||
document = self.state.document
|
document = self.state.document
|
||||||
if self.content:
|
if self.content:
|
||||||
return [document.reporter.warning(
|
return [
|
||||||
__('Graphviz directive cannot have both content and '
|
document.reporter.warning(
|
||||||
'a filename argument'), line=self.lineno)]
|
__(
|
||||||
|
'Graphviz directive cannot have both content and '
|
||||||
|
'a filename argument'
|
||||||
|
),
|
||||||
|
line=self.lineno,
|
||||||
|
)
|
||||||
|
]
|
||||||
argument = search_image_for_language(self.arguments[0], self.env)
|
argument = search_image_for_language(self.arguments[0], self.env)
|
||||||
rel_filename, filename = self.env.relfn2path(argument)
|
rel_filename, filename = self.env.relfn2path(argument)
|
||||||
self.env.note_dependency(rel_filename)
|
self.env.note_dependency(rel_filename)
|
||||||
@ -141,16 +149,23 @@ class Graphviz(SphinxDirective):
|
|||||||
with open(filename, encoding='utf-8') as fp:
|
with open(filename, encoding='utf-8') as fp:
|
||||||
dotcode = fp.read()
|
dotcode = fp.read()
|
||||||
except OSError:
|
except OSError:
|
||||||
return [document.reporter.warning(
|
return [
|
||||||
__('External Graphviz file %r not found or reading '
|
document.reporter.warning(
|
||||||
'it failed') % filename, line=self.lineno)]
|
__('External Graphviz file %r not found or reading it failed')
|
||||||
|
% filename,
|
||||||
|
line=self.lineno,
|
||||||
|
)
|
||||||
|
]
|
||||||
else:
|
else:
|
||||||
dotcode = '\n'.join(self.content)
|
dotcode = '\n'.join(self.content)
|
||||||
rel_filename = None
|
rel_filename = None
|
||||||
if not dotcode.strip():
|
if not dotcode.strip():
|
||||||
return [self.state_machine.reporter.warning(
|
return [
|
||||||
__('Ignoring "graphviz" directive without content.'),
|
self.state_machine.reporter.warning(
|
||||||
line=self.lineno)]
|
__('Ignoring "graphviz" directive without content.'),
|
||||||
|
line=self.lineno,
|
||||||
|
)
|
||||||
|
]
|
||||||
node = graphviz()
|
node = graphviz()
|
||||||
node['code'] = dotcode
|
node['code'] = dotcode
|
||||||
node['options'] = {'docname': self.env.docname}
|
node['options'] = {'docname': self.env.docname}
|
||||||
@ -198,8 +213,8 @@ class GraphvizSimple(SphinxDirective):
|
|||||||
|
|
||||||
def run(self) -> list[Node]:
|
def run(self) -> list[Node]:
|
||||||
node = graphviz()
|
node = graphviz()
|
||||||
node['code'] = '%s %s {\n%s\n}\n' % \
|
dot_code = '\n'.join(self.content)
|
||||||
(self.name, self.arguments[0], '\n'.join(self.content))
|
node['code'] = f'{self.name} {self.arguments[0]} {{\n{dot_code}\n}}\n'
|
||||||
node['options'] = {'docname': self.env.docname}
|
node['options'] = {'docname': self.env.docname}
|
||||||
if 'graphviz_dot' in self.options:
|
if 'graphviz_dot' in self.options:
|
||||||
node['options']['graphviz_dot'] = self.options['graphviz_dot']
|
node['options']['graphviz_dot'] = self.options['graphviz_dot']
|
||||||
@ -221,8 +236,9 @@ class GraphvizSimple(SphinxDirective):
|
|||||||
return [figure]
|
return [figure]
|
||||||
|
|
||||||
|
|
||||||
def fix_svg_relative_paths(self: HTML5Translator | LaTeXTranslator | TexinfoTranslator,
|
def fix_svg_relative_paths(
|
||||||
filepath: str) -> None:
|
self: HTML5Translator | LaTeXTranslator | TexinfoTranslator, filepath: str
|
||||||
|
) -> None:
|
||||||
"""Change relative links in generated svg files to be relative to imgpath."""
|
"""Change relative links in generated svg files to be relative to imgpath."""
|
||||||
tree = ET.parse(filepath) # NoQA: S314
|
tree = ET.parse(filepath) # NoQA: S314
|
||||||
root = tree.getroot()
|
root = tree.getroot()
|
||||||
@ -239,7 +255,7 @@ def fix_svg_relative_paths(self: HTML5Translator | LaTeXTranslator | TexinfoTran
|
|||||||
# not a relative link
|
# not a relative link
|
||||||
continue
|
continue
|
||||||
|
|
||||||
docname = self.builder.env.path2doc(self.document["source"])
|
docname = self.builder.env.path2doc(self.document['source'])
|
||||||
if docname is None:
|
if docname is None:
|
||||||
# This shouldn't happen!
|
# This shouldn't happen!
|
||||||
continue
|
continue
|
||||||
@ -257,18 +273,26 @@ def fix_svg_relative_paths(self: HTML5Translator | LaTeXTranslator | TexinfoTran
|
|||||||
tree.write(filepath)
|
tree.write(filepath)
|
||||||
|
|
||||||
|
|
||||||
def render_dot(self: HTML5Translator | LaTeXTranslator | TexinfoTranslator,
|
def render_dot(
|
||||||
code: str, options: dict, format: str,
|
self: HTML5Translator | LaTeXTranslator | TexinfoTranslator,
|
||||||
prefix: str = 'graphviz', filename: str | None = None,
|
code: str,
|
||||||
) -> tuple[str | None, str | None]:
|
options: dict,
|
||||||
|
format: str,
|
||||||
|
prefix: str = 'graphviz',
|
||||||
|
filename: str | None = None,
|
||||||
|
) -> tuple[str | None, str | None]:
|
||||||
"""Render graphviz code into a PNG or PDF output file."""
|
"""Render graphviz code into a PNG or PDF output file."""
|
||||||
graphviz_dot = options.get('graphviz_dot', self.builder.config.graphviz_dot)
|
graphviz_dot = options.get('graphviz_dot', self.builder.config.graphviz_dot)
|
||||||
if not graphviz_dot:
|
if not graphviz_dot:
|
||||||
raise GraphvizError(
|
raise GraphvizError(
|
||||||
__('graphviz_dot executable path must be set! %r') % graphviz_dot,
|
__('graphviz_dot executable path must be set! %r') % graphviz_dot,
|
||||||
)
|
)
|
||||||
hashkey = (code + str(options) + str(graphviz_dot) +
|
hashkey = ''.join((
|
||||||
str(self.builder.config.graphviz_dot_args)).encode()
|
code,
|
||||||
|
str(options),
|
||||||
|
str(graphviz_dot),
|
||||||
|
str(self.builder.config.graphviz_dot_args),
|
||||||
|
)).encode()
|
||||||
|
|
||||||
fname = f'{prefix}-{sha1(hashkey, usedforsecurity=False).hexdigest()}.{format}'
|
fname = f'{prefix}-{sha1(hashkey, usedforsecurity=False).hexdigest()}.{format}'
|
||||||
relfn = posixpath.join(self.builder.imgpath, fname)
|
relfn = posixpath.join(self.builder.imgpath, fname)
|
||||||
@ -277,8 +301,7 @@ def render_dot(self: HTML5Translator | LaTeXTranslator | TexinfoTranslator,
|
|||||||
if os.path.isfile(outfn):
|
if os.path.isfile(outfn):
|
||||||
return relfn, outfn
|
return relfn, outfn
|
||||||
|
|
||||||
if (hasattr(self.builder, '_graphviz_warned_dot') and
|
if getattr(self.builder, '_graphviz_warned_dot', {}).get(graphviz_dot):
|
||||||
self.builder._graphviz_warned_dot.get(graphviz_dot)):
|
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
ensuredir(os.path.dirname(outfn))
|
ensuredir(os.path.dirname(outfn))
|
||||||
@ -294,24 +317,34 @@ def render_dot(self: HTML5Translator | LaTeXTranslator | TexinfoTranslator,
|
|||||||
cwd = os.path.dirname(os.path.join(self.builder.srcdir, docname))
|
cwd = os.path.dirname(os.path.join(self.builder.srcdir, docname))
|
||||||
|
|
||||||
if format == 'png':
|
if format == 'png':
|
||||||
dot_args.extend(['-Tcmapx', '-o%s.map' % outfn])
|
dot_args.extend(['-Tcmapx', f'-o{outfn}.map'])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ret = subprocess.run(dot_args, input=code.encode(), capture_output=True,
|
ret = subprocess.run(
|
||||||
cwd=cwd, check=True)
|
dot_args, input=code.encode(), capture_output=True, cwd=cwd, check=True
|
||||||
|
)
|
||||||
except OSError:
|
except OSError:
|
||||||
logger.warning(__('dot command %r cannot be run (needed for graphviz '
|
logger.warning(
|
||||||
'output), check the graphviz_dot setting'), graphviz_dot)
|
__(
|
||||||
|
'dot command %r cannot be run (needed for graphviz '
|
||||||
|
'output), check the graphviz_dot setting'
|
||||||
|
),
|
||||||
|
graphviz_dot,
|
||||||
|
)
|
||||||
if not hasattr(self.builder, '_graphviz_warned_dot'):
|
if not hasattr(self.builder, '_graphviz_warned_dot'):
|
||||||
self.builder._graphviz_warned_dot = {} # type: ignore[union-attr]
|
self.builder._graphviz_warned_dot = {} # type: ignore[union-attr]
|
||||||
self.builder._graphviz_warned_dot[graphviz_dot] = True
|
self.builder._graphviz_warned_dot[graphviz_dot] = True
|
||||||
return None, None
|
return None, None
|
||||||
except CalledProcessError as exc:
|
except CalledProcessError as exc:
|
||||||
raise GraphvizError(__('dot exited with error:\n[stderr]\n%r\n'
|
raise GraphvizError(
|
||||||
'[stdout]\n%r') % (exc.stderr, exc.stdout)) from exc
|
__('dot exited with error:\n[stderr]\n%r\n[stdout]\n%r')
|
||||||
|
% (exc.stderr, exc.stdout)
|
||||||
|
) from exc
|
||||||
if not os.path.isfile(outfn):
|
if not os.path.isfile(outfn):
|
||||||
raise GraphvizError(__('dot did not produce an output file:\n[stderr]\n%r\n'
|
raise GraphvizError(
|
||||||
'[stdout]\n%r') % (ret.stderr, ret.stdout))
|
__('dot did not produce an output file:\n[stderr]\n%r\n[stdout]\n%r')
|
||||||
|
% (ret.stderr, ret.stdout)
|
||||||
|
)
|
||||||
|
|
||||||
if format == 'svg':
|
if format == 'svg':
|
||||||
fix_svg_relative_paths(self, outfn)
|
fix_svg_relative_paths(self, outfn)
|
||||||
@ -319,15 +352,23 @@ def render_dot(self: HTML5Translator | LaTeXTranslator | TexinfoTranslator,
|
|||||||
return relfn, outfn
|
return relfn, outfn
|
||||||
|
|
||||||
|
|
||||||
def render_dot_html(self: HTML5Translator, node: graphviz, code: str, options: dict,
|
def render_dot_html(
|
||||||
prefix: str = 'graphviz', imgcls: str | None = None,
|
self: HTML5Translator,
|
||||||
alt: str | None = None, filename: str | None = None,
|
node: graphviz,
|
||||||
) -> tuple[str, str]:
|
code: str,
|
||||||
|
options: dict,
|
||||||
|
prefix: str = 'graphviz',
|
||||||
|
imgcls: str | None = None,
|
||||||
|
alt: str | None = None,
|
||||||
|
filename: str | None = None,
|
||||||
|
) -> tuple[str, str]:
|
||||||
format = self.builder.config.graphviz_output_format
|
format = self.builder.config.graphviz_output_format
|
||||||
try:
|
try:
|
||||||
if format not in {'png', 'svg'}:
|
if format not in {'png', 'svg'}:
|
||||||
raise GraphvizError(__("graphviz_output_format must be one of 'png', "
|
raise GraphvizError(
|
||||||
"'svg', but is %r") % format)
|
__("graphviz_output_format must be one of 'png', 'svg', but is %r")
|
||||||
|
% format
|
||||||
|
)
|
||||||
fname, outfn = render_dot(self, code, options, format, prefix, filename)
|
fname, outfn = render_dot(self, code, options, format, prefix, filename)
|
||||||
except GraphvizError as exc:
|
except GraphvizError as exc:
|
||||||
logger.warning(__('dot code %r: %s'), code, exc)
|
logger.warning(__('dot code %r: %s'), code, exc)
|
||||||
@ -342,31 +383,33 @@ def render_dot_html(self: HTML5Translator, node: graphviz, code: str, options: d
|
|||||||
if alt is None:
|
if alt is None:
|
||||||
alt = node.get('alt', self.encode(code).strip())
|
alt = node.get('alt', self.encode(code).strip())
|
||||||
if 'align' in node:
|
if 'align' in node:
|
||||||
self.body.append('<div align="%s" class="align-%s">' %
|
align = node['align']
|
||||||
(node['align'], node['align']))
|
self.body.append(f'<div align="{align}" class="align-{align}">')
|
||||||
if format == 'svg':
|
if format == 'svg':
|
||||||
self.body.append('<div class="graphviz">')
|
self.body.append('<div class="graphviz">')
|
||||||
self.body.append('<object data="%s" type="image/svg+xml" class="%s">\n' %
|
self.body.append(
|
||||||
(fname, imgcls))
|
f'<object data="{fname}" type="image/svg+xml" class="{imgcls}">\n'
|
||||||
self.body.append('<p class="warning">%s</p>' % alt)
|
)
|
||||||
|
self.body.append(f'<p class="warning">{alt}</p>')
|
||||||
self.body.append('</object></div>\n')
|
self.body.append('</object></div>\n')
|
||||||
else:
|
else:
|
||||||
assert outfn is not None
|
assert outfn is not None
|
||||||
with open(outfn + '.map', encoding='utf-8') as mapfile:
|
with open(f'{outfn}.map', encoding='utf-8') as mapfile:
|
||||||
imgmap = ClickableMapDefinition(outfn + '.map', mapfile.read(), dot=code)
|
map_content = mapfile.read()
|
||||||
if imgmap.clickable:
|
imgmap = ClickableMapDefinition(f'{outfn}.map', map_content, dot=code)
|
||||||
# has a map
|
if imgmap.clickable:
|
||||||
self.body.append('<div class="graphviz">')
|
# has a map
|
||||||
self.body.append('<img src="%s" alt="%s" usemap="#%s" class="%s" />' %
|
self.body.append('<div class="graphviz">')
|
||||||
(fname, alt, imgmap.id, imgcls))
|
self.body.append(
|
||||||
self.body.append('</div>\n')
|
f'<img src="{fname}" alt="{alt}" usemap="#{imgmap.id}" class="{imgcls}" />'
|
||||||
self.body.append(imgmap.generate_clickable_map())
|
)
|
||||||
else:
|
self.body.append('</div>\n')
|
||||||
# nothing in image map
|
self.body.append(imgmap.generate_clickable_map())
|
||||||
self.body.append('<div class="graphviz">')
|
else:
|
||||||
self.body.append('<img src="%s" alt="%s" class="%s" />' %
|
# nothing in image map
|
||||||
(fname, alt, imgcls))
|
self.body.append('<div class="graphviz">')
|
||||||
self.body.append('</div>\n')
|
self.body.append(f'<img src="{fname}" alt="{alt}" class="{imgcls}" />')
|
||||||
|
self.body.append('</div>\n')
|
||||||
if 'align' in node:
|
if 'align' in node:
|
||||||
self.body.append('</div>\n')
|
self.body.append('</div>\n')
|
||||||
|
|
||||||
@ -374,12 +417,19 @@ def render_dot_html(self: HTML5Translator, node: graphviz, code: str, options: d
|
|||||||
|
|
||||||
|
|
||||||
def html_visit_graphviz(self: HTML5Translator, node: graphviz) -> None:
|
def html_visit_graphviz(self: HTML5Translator, node: graphviz) -> None:
|
||||||
render_dot_html(self, node, node['code'], node['options'], filename=node.get('filename'))
|
render_dot_html(
|
||||||
|
self, node, node['code'], node['options'], filename=node.get('filename')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def render_dot_latex(self: LaTeXTranslator, node: graphviz, code: str,
|
def render_dot_latex(
|
||||||
options: dict, prefix: str = 'graphviz', filename: str | None = None,
|
self: LaTeXTranslator,
|
||||||
) -> None:
|
node: graphviz,
|
||||||
|
code: str,
|
||||||
|
options: dict,
|
||||||
|
prefix: str = 'graphviz',
|
||||||
|
filename: str | None = None,
|
||||||
|
) -> None:
|
||||||
try:
|
try:
|
||||||
fname, outfn = render_dot(self, code, options, 'pdf', prefix, filename)
|
fname, outfn = render_dot(self, code, options, 'pdf', prefix, filename)
|
||||||
except GraphvizError as exc:
|
except GraphvizError as exc:
|
||||||
@ -401,22 +451,29 @@ def render_dot_latex(self: LaTeXTranslator, node: graphviz, code: str,
|
|||||||
elif node['align'] == 'center':
|
elif node['align'] == 'center':
|
||||||
pre = r'{\hfill'
|
pre = r'{\hfill'
|
||||||
post = r'\hspace*{\fill}}'
|
post = r'\hspace*{\fill}}'
|
||||||
self.body.append('\n%s' % pre)
|
self.body.append(f'\n{pre}')
|
||||||
|
|
||||||
self.body.append(r'\sphinxincludegraphics[]{%s}' % fname)
|
self.body.append(r'\sphinxincludegraphics[]{%s}' % fname)
|
||||||
|
|
||||||
if not is_inline:
|
if not is_inline:
|
||||||
self.body.append('%s\n' % post)
|
self.body.append(f'{post}\n')
|
||||||
|
|
||||||
raise nodes.SkipNode
|
raise nodes.SkipNode
|
||||||
|
|
||||||
|
|
||||||
def latex_visit_graphviz(self: LaTeXTranslator, node: graphviz) -> None:
|
def latex_visit_graphviz(self: LaTeXTranslator, node: graphviz) -> None:
|
||||||
render_dot_latex(self, node, node['code'], node['options'], filename=node.get('filename'))
|
render_dot_latex(
|
||||||
|
self, node, node['code'], node['options'], filename=node.get('filename')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def render_dot_texinfo(self: TexinfoTranslator, node: graphviz, code: str,
|
def render_dot_texinfo(
|
||||||
options: dict, prefix: str = 'graphviz') -> None:
|
self: TexinfoTranslator,
|
||||||
|
node: graphviz,
|
||||||
|
code: str,
|
||||||
|
options: dict,
|
||||||
|
prefix: str = 'graphviz',
|
||||||
|
) -> None:
|
||||||
try:
|
try:
|
||||||
fname, outfn = render_dot(self, code, options, 'png', prefix)
|
fname, outfn = render_dot(self, code, options, 'png', prefix)
|
||||||
except GraphvizError as exc:
|
except GraphvizError as exc:
|
||||||
@ -453,12 +510,14 @@ def on_config_inited(_app: Sphinx, config: Config) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def setup(app: Sphinx) -> ExtensionMetadata:
|
def setup(app: Sphinx) -> ExtensionMetadata:
|
||||||
app.add_node(graphviz,
|
app.add_node(
|
||||||
html=(html_visit_graphviz, None),
|
graphviz,
|
||||||
latex=(latex_visit_graphviz, None),
|
html=(html_visit_graphviz, None),
|
||||||
texinfo=(texinfo_visit_graphviz, None),
|
latex=(latex_visit_graphviz, None),
|
||||||
text=(text_visit_graphviz, None),
|
texinfo=(texinfo_visit_graphviz, None),
|
||||||
man=(man_visit_graphviz, None))
|
text=(text_visit_graphviz, None),
|
||||||
|
man=(man_visit_graphviz, None),
|
||||||
|
)
|
||||||
app.add_directive('graphviz', Graphviz)
|
app.add_directive('graphviz', Graphviz)
|
||||||
app.add_directive('graph', GraphvizSimple)
|
app.add_directive('graph', GraphvizSimple)
|
||||||
app.add_directive('digraph', GraphvizSimple)
|
app.add_directive('digraph', GraphvizSimple)
|
||||||
|
Loading…
Reference in New Issue
Block a user