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('