diff --git a/.ruff.toml b/.ruff.toml index af6d2608148..56003932b14 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -415,7 +415,6 @@ exclude = [ "sphinx/domains/python/_object.py", "sphinx/domains/rst.py", "sphinx/domains/std/__init__.py", - "sphinx/ext/graphviz.py", "sphinx/ext/ifconfig.py", "sphinx/ext/imgconverter.py", "sphinx/ext/imgmath.py", diff --git a/sphinx/ext/graphviz.py b/sphinx/ext/graphviz.py index eb85f2d4c99..623aca88364 100644 --- a/sphinx/ext/graphviz.py +++ b/sphinx/ext/graphviz.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 @@ -62,14 +61,15 @@ def __init__(self, filename: str, content: str, dot: str = '') -> None: def parse(self, dot: str) -> None: matched = self.maptag_re.match(self.content[0]) 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) if self.id == '%3': # graphviz generates wrong ID if graph name not specified # https://gitlab.com/graphviz/graphviz/issues/1327 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) for line in self.content: @@ -91,7 +91,9 @@ class graphviz(nodes.General, nodes.Inline, nodes.Element): 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) if 'align' in node: figure_node['align'] = node.attributes.pop('align') @@ -131,9 +133,15 @@ def run(self) -> list[Node]: if self.arguments: document = self.state.document if self.content: - return [document.reporter.warning( - __('Graphviz directive cannot have both content and ' - 'a filename argument'), line=self.lineno)] + return [ + document.reporter.warning( + __( + 'Graphviz directive cannot have both content and ' + 'a filename argument' + ), + line=self.lineno, + ) + ] argument = search_image_for_language(self.arguments[0], self.env) rel_filename, filename = self.env.relfn2path(argument) self.env.note_dependency(rel_filename) @@ -141,16 +149,23 @@ def run(self) -> list[Node]: with open(filename, encoding='utf-8') as fp: dotcode = fp.read() except OSError: - return [document.reporter.warning( - __('External Graphviz file %r not found or reading ' - 'it failed') % filename, line=self.lineno)] + return [ + document.reporter.warning( + __('External Graphviz file %r not found or reading it failed') + % filename, + line=self.lineno, + ) + ] else: dotcode = '\n'.join(self.content) rel_filename = None if not dotcode.strip(): - return [self.state_machine.reporter.warning( - __('Ignoring "graphviz" directive without content.'), - line=self.lineno)] + return [ + self.state_machine.reporter.warning( + __('Ignoring "graphviz" directive without content.'), + line=self.lineno, + ) + ] node = graphviz() node['code'] = dotcode node['options'] = {'docname': self.env.docname} @@ -198,8 +213,8 @@ class GraphvizSimple(SphinxDirective): def run(self) -> list[Node]: node = graphviz() - node['code'] = '%s %s {\n%s\n}\n' % \ - (self.name, self.arguments[0], '\n'.join(self.content)) + dot_code = '\n'.join(self.content) + node['code'] = f'{self.name} {self.arguments[0]} {{\n{dot_code}\n}}\n' node['options'] = {'docname': self.env.docname} if 'graphviz_dot' in self.options: node['options']['graphviz_dot'] = self.options['graphviz_dot'] @@ -221,8 +236,9 @@ def run(self) -> list[Node]: return [figure] -def fix_svg_relative_paths(self: HTML5Translator | LaTeXTranslator | TexinfoTranslator, - filepath: str) -> None: +def fix_svg_relative_paths( + self: HTML5Translator | LaTeXTranslator | TexinfoTranslator, filepath: str +) -> None: """Change relative links in generated svg files to be relative to imgpath.""" tree = ET.parse(filepath) # NoQA: S314 root = tree.getroot() @@ -239,7 +255,7 @@ def fix_svg_relative_paths(self: HTML5Translator | LaTeXTranslator | TexinfoTran # not a relative link continue - docname = self.builder.env.path2doc(self.document["source"]) + docname = self.builder.env.path2doc(self.document['source']) if docname is None: # This shouldn't happen! continue @@ -257,18 +273,26 @@ def fix_svg_relative_paths(self: HTML5Translator | LaTeXTranslator | TexinfoTran tree.write(filepath) -def render_dot(self: HTML5Translator | LaTeXTranslator | TexinfoTranslator, - code: str, options: dict, format: str, - prefix: str = 'graphviz', filename: str | None = None, - ) -> tuple[str | None, str | None]: +def render_dot( + self: HTML5Translator | LaTeXTranslator | TexinfoTranslator, + code: str, + 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.""" graphviz_dot = options.get('graphviz_dot', self.builder.config.graphviz_dot) if not graphviz_dot: raise GraphvizError( __('graphviz_dot executable path must be set! %r') % graphviz_dot, ) - hashkey = (code + str(options) + str(graphviz_dot) + - str(self.builder.config.graphviz_dot_args)).encode() + hashkey = ''.join(( + code, + str(options), + str(graphviz_dot), + str(self.builder.config.graphviz_dot_args), + )).encode() fname = f'{prefix}-{sha1(hashkey, usedforsecurity=False).hexdigest()}.{format}' relfn = posixpath.join(self.builder.imgpath, fname) @@ -277,8 +301,7 @@ def render_dot(self: HTML5Translator | LaTeXTranslator | TexinfoTranslator, if os.path.isfile(outfn): return relfn, outfn - if (hasattr(self.builder, '_graphviz_warned_dot') and - self.builder._graphviz_warned_dot.get(graphviz_dot)): + if getattr(self.builder, '_graphviz_warned_dot', {}).get(graphviz_dot): return None, None 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)) if format == 'png': - dot_args.extend(['-Tcmapx', '-o%s.map' % outfn]) + dot_args.extend(['-Tcmapx', f'-o{outfn}.map']) try: - ret = subprocess.run(dot_args, input=code.encode(), capture_output=True, - cwd=cwd, check=True) + ret = subprocess.run( + dot_args, input=code.encode(), capture_output=True, cwd=cwd, check=True + ) except OSError: - logger.warning(__('dot command %r cannot be run (needed for graphviz ' - 'output), check the graphviz_dot setting'), graphviz_dot) + logger.warning( + __( + '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'): self.builder._graphviz_warned_dot = {} # type: ignore[union-attr] self.builder._graphviz_warned_dot[graphviz_dot] = True return None, None except CalledProcessError as exc: - raise GraphvizError(__('dot exited with error:\n[stderr]\n%r\n' - '[stdout]\n%r') % (exc.stderr, exc.stdout)) from exc + raise GraphvizError( + __('dot exited with error:\n[stderr]\n%r\n[stdout]\n%r') + % (exc.stderr, exc.stdout) + ) from exc if not os.path.isfile(outfn): - raise GraphvizError(__('dot did not produce an output file:\n[stderr]\n%r\n' - '[stdout]\n%r') % (ret.stderr, ret.stdout)) + raise GraphvizError( + __('dot did not produce an output file:\n[stderr]\n%r\n[stdout]\n%r') + % (ret.stderr, ret.stdout) + ) if format == 'svg': fix_svg_relative_paths(self, outfn) @@ -319,15 +352,23 @@ def render_dot(self: HTML5Translator | LaTeXTranslator | TexinfoTranslator, return relfn, outfn -def render_dot_html(self: HTML5Translator, node: graphviz, code: str, options: dict, - prefix: str = 'graphviz', imgcls: str | None = None, - alt: str | None = None, filename: str | None = None, - ) -> tuple[str, str]: +def render_dot_html( + self: HTML5Translator, + node: graphviz, + 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 try: if format not in {'png', 'svg'}: - raise GraphvizError(__("graphviz_output_format must be one of 'png', " - "'svg', but is %r") % format) + raise GraphvizError( + __("graphviz_output_format must be one of 'png', 'svg', but is %r") + % format + ) fname, outfn = render_dot(self, code, options, format, prefix, filename) except GraphvizError as 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: alt = node.get('alt', self.encode(code).strip()) if 'align' in node: - self.body.append('
' % - (node['align'], node['align'])) + align = node['align'] + self.body.append(f'
') if format == 'svg': self.body.append('
') - self.body.append('\n' % - (fname, imgcls)) - self.body.append('

%s

' % alt) + self.body.append( + f'\n' + ) + self.body.append(f'

{alt}

') self.body.append('
\n') else: assert outfn is not None - with open(outfn + '.map', encoding='utf-8') as mapfile: - imgmap = ClickableMapDefinition(outfn + '.map', mapfile.read(), dot=code) - if imgmap.clickable: - # has a map - self.body.append('
') - self.body.append('%s' % - (fname, alt, imgmap.id, imgcls)) - self.body.append('
\n') - self.body.append(imgmap.generate_clickable_map()) - else: - # nothing in image map - self.body.append('
') - self.body.append('%s' % - (fname, alt, imgcls)) - self.body.append('
\n') + with open(f'{outfn}.map', encoding='utf-8') as mapfile: + map_content = mapfile.read() + imgmap = ClickableMapDefinition(f'{outfn}.map', map_content, dot=code) + if imgmap.clickable: + # has a map + self.body.append('
') + self.body.append( + f'{alt}' + ) + self.body.append('
\n') + self.body.append(imgmap.generate_clickable_map()) + else: + # nothing in image map + self.body.append('
') + self.body.append(f'{alt}') + self.body.append('
\n') if 'align' in node: self.body.append('\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: - render_dot_html(self, node, node['code'], node['options'], filename=node.get('filename')) - - -def render_dot_latex(self: LaTeXTranslator, node: graphviz, code: str, - options: dict, prefix: str = 'graphviz', filename: str | None = None, - ) -> None: + render_dot_html( + self, node, node['code'], node['options'], filename=node.get('filename') + ) + + +def render_dot_latex( + self: LaTeXTranslator, + node: graphviz, + code: str, + options: dict, + prefix: str = 'graphviz', + filename: str | None = None, +) -> None: try: fname, outfn = render_dot(self, code, options, 'pdf', prefix, filename) except GraphvizError as exc: @@ -401,22 +451,29 @@ def render_dot_latex(self: LaTeXTranslator, node: graphviz, code: str, elif node['align'] == 'center': pre = r'{\hfill' post = r'\hspace*{\fill}}' - self.body.append('\n%s' % pre) + self.body.append(f'\n{pre}') self.body.append(r'\sphinxincludegraphics[]{%s}' % fname) if not is_inline: - self.body.append('%s\n' % post) + self.body.append(f'{post}\n') raise nodes.SkipNode def latex_visit_graphviz(self: LaTeXTranslator, node: graphviz) -> None: - render_dot_latex(self, node, node['code'], node['options'], filename=node.get('filename')) - - -def render_dot_texinfo(self: TexinfoTranslator, node: graphviz, code: str, - options: dict, prefix: str = 'graphviz') -> None: + render_dot_latex( + self, node, node['code'], node['options'], filename=node.get('filename') + ) + + +def render_dot_texinfo( + self: TexinfoTranslator, + node: graphviz, + code: str, + options: dict, + prefix: str = 'graphviz', +) -> None: try: fname, outfn = render_dot(self, code, options, 'png', prefix) except GraphvizError as exc: @@ -453,12 +510,14 @@ def on_config_inited(_app: Sphinx, config: Config) -> None: def setup(app: Sphinx) -> ExtensionMetadata: - app.add_node(graphviz, - html=(html_visit_graphviz, None), - latex=(latex_visit_graphviz, None), - texinfo=(texinfo_visit_graphviz, None), - text=(text_visit_graphviz, None), - man=(man_visit_graphviz, None)) + app.add_node( + graphviz, + html=(html_visit_graphviz, None), + latex=(latex_visit_graphviz, None), + texinfo=(texinfo_visit_graphviz, None), + text=(text_visit_graphviz, None), + man=(man_visit_graphviz, None), + ) app.add_directive('graphviz', Graphviz) app.add_directive('graph', GraphvizSimple) app.add_directive('digraph', GraphvizSimple)