From f7fbe1a172e89286591a987e9c5a35cbc2b75228 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:11:30 +0000 Subject: [PATCH 1/7] Initial plan From 9c0684f2ee8545c7af3c96991b0d9bc4b60f3c62 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:15:25 +0000 Subject: [PATCH 2/7] Use svg by default instead of png in gdot directive Co-authored-by: xadupre <22452781+xadupre@users.noreply.github.com> --- sphinx_runpython/gdot/sphinx_gdot_extension.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx_runpython/gdot/sphinx_gdot_extension.py b/sphinx_runpython/gdot/sphinx_gdot_extension.py index b9b7083..edc5e4b 100644 --- a/sphinx_runpython/gdot/sphinx_gdot_extension.py +++ b/sphinx_runpython/gdot/sphinx_gdot_extension.py @@ -111,7 +111,7 @@ def run(self): if "format" in self.options: # noqa: SIM401 format = self.options["format"] else: - format = "png" + format = "svg" url = self.options.get("url", "local") bool_set_ = (True, 1, "True", "1", "true", "") process = "process" in self.options and self.options["process"] in bool_set_ From 2e928951c3a2a705a835a9a44b8d90c853c10619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20Dupr=C3=A9?= Date: Tue, 3 Mar 2026 12:56:52 +0100 Subject: [PATCH 3/7] update version --- CHANGELOGS.rst | 5 +++++ _doc/index.rst | 1 + sphinx_runpython/__init__.py | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOGS.rst b/CHANGELOGS.rst index f231c6b..08cc2f9 100644 --- a/CHANGELOGS.rst +++ b/CHANGELOGS.rst @@ -1,6 +1,11 @@ Change Logs =========== +0.4.2 ++++++ + +* :pr:`47`: use svg by default with gdot + 0.4.1 +++++ diff --git a/_doc/index.rst b/_doc/index.rst index 003082c..7724d9c 100644 --- a/_doc/index.rst +++ b/_doc/index.rst @@ -54,6 +54,7 @@ Sources available on Older versions ++++++++++++++ +* `0.4.2 <../v0.4.2/index.html>`_ * `0.4.1 <../v0.4.1/index.html>`_ * `0.4.0 <../v0.4.0/index.html>`_ * `0.3.0 <../v0.3.0/index.html>`_ diff --git a/sphinx_runpython/__init__.py b/sphinx_runpython/__init__.py index 12a3dfd..104ec78 100644 --- a/sphinx_runpython/__init__.py +++ b/sphinx_runpython/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.4.1" +__version__ = "0.4.2" __author__ = "Xavier Dupré" __github__ = "https://github.com/sdpython/sphinx-runpython" __url__ = "https://sdpython.github.io/doc/sphinx-runpython/dev/" From 170168d1629e67f1636e3042e4865b4a9938d862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20Dupr=C3=A9?= Date: Tue, 3 Mar 2026 13:46:43 +0100 Subject: [PATCH 4/7] fix svg --- _unittests/ut_gdot/test_gdot_extension.py | 10 ++++--- .../gdot/sphinx_gdot_extension.py | 29 +++++++++---------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/_unittests/ut_gdot/test_gdot_extension.py b/_unittests/ut_gdot/test_gdot_extension.py index 39bee87..3490913 100644 --- a/_unittests/ut_gdot/test_gdot_extension.py +++ b/_unittests/ut_gdot/test_gdot_extension.py @@ -16,6 +16,7 @@ def test_gdot1(self): before .. gdot:: + :format: png digraph foo { "bar" -> "baz"; @@ -38,6 +39,7 @@ def test_gdot2(self): .. gdot:: :script: + :format: png print('''digraph foo { HbarH -> HbazH; }'''.replace("H", '"')) @@ -56,6 +58,7 @@ def test_gdot2_split(self): .. gdot:: :script: BEGIN + :format: png print('''...BEGINdigraph foo { HbarH -> HbazH; }'''.replace("H", '"')) @@ -86,8 +89,8 @@ def test_gdot3_svg(self): content = rst2html( content, writer_name="html", new_extensions=["sphinx_runpython.gdot"] ) - self.assertIn("document.getElementById('gdot-", content) - self.assertIn('foo {\\n \\"bar\\" -> \\"baz\\";\\n}");', content) + self.assertIn("digraph foo {", content) + print(content) @ignore_warnings(PendingDeprecationWarning) def test_gdot3_svg_process(self): @@ -108,8 +111,7 @@ def test_gdot3_svg_process(self): content = rst2html( content, writer_name="html", new_extensions=["sphinx_runpython.gdot"] ) - self.assertIn("document.getElementById('gdot-", content) - self.assertIn('foo {\\n \\"bar\\" -> \\"baz\\";\\n}");', content) + self.assertIn("digraph foo {", content) @unittest.skipIf(sys.platform != "linux", reason="Missing dependency.") @ignore_warnings(PendingDeprecationWarning) diff --git a/sphinx_runpython/gdot/sphinx_gdot_extension.py b/sphinx_runpython/gdot/sphinx_gdot_extension.py index edc5e4b..86e716a 100644 --- a/sphinx_runpython/gdot/sphinx_gdot_extension.py +++ b/sphinx_runpython/gdot/sphinx_gdot_extension.py @@ -3,7 +3,7 @@ from docutils import nodes from docutils.parsers.rst import directives, Directive import sphinx -from sphinx.ext.graphviz import latex_visit_graphviz, text_visit_graphviz +from sphinx.ext.graphviz import latex_visit_graphviz, text_visit_graphviz, graphviz from ..ext_helper import get_env_state_info from ..ext_io_helper import download_requirejs, get_url_content_timeout from ..runpython.sphinx_runpython_extension import run_python_script @@ -104,9 +104,7 @@ class GDotDirective(Directive): _default_url = "https://cdnjs.cloudflare.com/ajax/libs/viz.js/1.8.0/viz-lite.js" def run(self): - """ - Builds the collapse text. - """ + """Builds the collapse text.""" # retrieves the parameters if "format" in self.options: # noqa: SIM401 format = self.options["format"] @@ -181,16 +179,19 @@ def run(self): logger.warning("[gdot] too many output lines %s", content) content = spl[-1] - node = gdot_node( - format=format, code=content, url=url, options={"docname": docname} - ) + if format == "svg": + node = graphviz() + node["code"] = content + node["options"] = {"docname": docname} + else: + node = gdot_node( + format=format, code=content, url=url, options={"docname": docname} + ) return [node] def visit_gdot_node_rst(self, node): - """ - visit collapse_node - """ + """visit collapse_node""" self.new_state(0) self.add_text(".. gdot::" + self.nl) if node["format"] != "?": @@ -203,17 +204,13 @@ def visit_gdot_node_rst(self, node): def depart_gdot_node_rst(self, node): - """ - depart collapse_node - """ + """depart collapse_node""" self.end_state() self.end_state(wrap=False) def visit_gdot_node_html_svg(self, node): - """ - visit collapse_node - """ + """visit collapse_node""" def process(text): text = text.replace("\\", "\\\\") From 4a9332c331731a42ff7ab9fb011186e37661004a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20Dupr=C3=A9?= Date: Tue, 3 Mar 2026 14:03:00 +0100 Subject: [PATCH 5/7] fix svg --- _unittests/ut_gdot/test_gdot_extension.py | 7 +- .../gdot/sphinx_gdot_extension.py | 93 +++++++++++++++++-- 2 files changed, 88 insertions(+), 12 deletions(-) diff --git a/_unittests/ut_gdot/test_gdot_extension.py b/_unittests/ut_gdot/test_gdot_extension.py index 3490913..70dc1fb 100644 --- a/_unittests/ut_gdot/test_gdot_extension.py +++ b/_unittests/ut_gdot/test_gdot_extension.py @@ -68,8 +68,9 @@ def test_gdot2_split(self): content = rst2html( content, writer_name="rst", new_extensions=["sphinx_runpython.gdot"] ) - self.assertIn('digraph foo { "bar" -> "baz"; }', content) + self.assertIn("svg", content) self.assertNotIn("BEGIN", content) + self.assertNotIn("png", content) @ignore_warnings(PendingDeprecationWarning) def test_gdot3_svg(self): @@ -89,8 +90,8 @@ def test_gdot3_svg(self): content = rst2html( content, writer_name="html", new_extensions=["sphinx_runpython.gdot"] ) - self.assertIn("digraph foo {", content) - print(content) + self.assertIn("svg", content) + self.assertNotIn("png", content) @ignore_warnings(PendingDeprecationWarning) def test_gdot3_svg_process(self): diff --git a/sphinx_runpython/gdot/sphinx_gdot_extension.py b/sphinx_runpython/gdot/sphinx_gdot_extension.py index 86e716a..05b7eca 100644 --- a/sphinx_runpython/gdot/sphinx_gdot_extension.py +++ b/sphinx_runpython/gdot/sphinx_gdot_extension.py @@ -2,8 +2,16 @@ import logging from docutils import nodes from docutils.parsers.rst import directives, Directive +from typing import Any import sphinx -from sphinx.ext.graphviz import latex_visit_graphviz, text_visit_graphviz, graphviz +from sphinx.ext.graphviz import ( + latex_visit_graphviz, + text_visit_graphviz, + render_dot, + GraphvizError, + ClickableMapDefinition, + __, +) from ..ext_helper import get_env_state_info from ..ext_io_helper import download_requirejs, get_url_content_timeout from ..runpython.sphinx_runpython_extension import run_python_script @@ -179,14 +187,13 @@ def run(self): logger.warning("[gdot] too many output lines %s", content) content = spl[-1] - if format == "svg": - node = graphviz() - node["code"] = content - node["options"] = {"docname": docname} - else: - node = gdot_node( - format=format, code=content, url=url, options={"docname": docname} - ) + node = gdot_node( + format=format, + code=content, + url=url, + options={"docname": docname}, + use_sphinx_graphviz=True, + ) return [node] @@ -209,8 +216,76 @@ def depart_gdot_node_rst(self, node): self.end_state(wrap=False) +def render_dot_html( + self, + node: gdot_node, + code: str, + options: dict[str, Any], + prefix: str = "gdot", + imgcls: str | None = None, + alt: str | None = None, + filename: str | None = None, + format: str = "svg", +) -> tuple[str, str]: + if format not in {"png", "svg"}: + logger = logging.getLogger(__name__) + logger.warning(__("format must be either 'png' or 'svg', but is %r"), format) + try: + fname, outfn = render_dot(self, code, options, format, prefix, filename) + except GraphvizError as exc: + logger.warning(__("dot code %r: %s"), code, exc) + raise nodes.SkipNode from exc + + classes = [imgcls, "graphviz", *node.get("classes", [])] + imgcls = " ".join(filter(None, classes)) + + if fname is None: + self.body.append(self.encode(code)) + else: + src = fname.as_posix() + if alt is None: + alt = node.get("alt", self.encode(code).strip()) + if "align" in node: + align = node["align"] + self.body.append(f'
') + if format == "svg": + self.body.append('
') + self.body.append( + f'\n' + ) + self.body.append(f'

{alt}

') + self.body.append("
\n") + else: + assert outfn is not None + 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") + + raise nodes.SkipNode + + def visit_gdot_node_html_svg(self, node): """visit collapse_node""" + if node["use_sphinx_graphviz"]: + render_dot_html( + self, node, node["code"], node["options"], filename=node.get("filename") + ) + return def process(text): text = text.replace("\\", "\\\\") From d2b8eb54b92c8e3bc06b9811f083fcd67f499a3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20Dupr=C3=A9?= Date: Tue, 3 Mar 2026 14:13:00 +0100 Subject: [PATCH 6/7] fix --- _doc/conf.py | 2 ++ _unittests/ut_gdot/test_gdot_extension.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/_doc/conf.py b/_doc/conf.py index d64cbb1..22c4aec 100644 --- a/_doc/conf.py +++ b/_doc/conf.py @@ -52,6 +52,8 @@ exclude_patterns = [] pygments_style = "sphinx" todo_include_todos = True +graphviz_output_format = "svg" +graphviz_dot_args = ["-Gbgcolor=transparent"] html_theme = "furo" html_theme_path = ["_static"] diff --git a/_unittests/ut_gdot/test_gdot_extension.py b/_unittests/ut_gdot/test_gdot_extension.py index 70dc1fb..971b79e 100644 --- a/_unittests/ut_gdot/test_gdot_extension.py +++ b/_unittests/ut_gdot/test_gdot_extension.py @@ -68,9 +68,9 @@ def test_gdot2_split(self): content = rst2html( content, writer_name="rst", new_extensions=["sphinx_runpython.gdot"] ) - self.assertIn("svg", content) + self.assertNotIn("svg", content) self.assertNotIn("BEGIN", content) - self.assertNotIn("png", content) + self.assertIn("png", content) @ignore_warnings(PendingDeprecationWarning) def test_gdot3_svg(self): From 4464e3cc9cb59246c9f12310d1330d18f56512c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20Dupr=C3=A9?= Date: Tue, 3 Mar 2026 23:36:38 +0100 Subject: [PATCH 7/7] fix --- _unittests/ut_gdot/test_gdot_extension.py | 9 +++++- sphinx_runpython/ext_test_case.py | 36 +++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/_unittests/ut_gdot/test_gdot_extension.py b/_unittests/ut_gdot/test_gdot_extension.py index 971b79e..d1ee7a3 100644 --- a/_unittests/ut_gdot/test_gdot_extension.py +++ b/_unittests/ut_gdot/test_gdot_extension.py @@ -2,7 +2,12 @@ import logging import sys from sphinx_runpython.process_rst import rst2html -from sphinx_runpython.ext_test_case import ExtTestCase, ignore_warnings +from sphinx_runpython.ext_test_case import ( + ExtTestCase, + ignore_warnings, + skipif_ci_apple, + skipif_ci_windows, +) class TestGDotExtension(ExtTestCase): @@ -73,6 +78,8 @@ def test_gdot2_split(self): self.assertIn("png", content) @ignore_warnings(PendingDeprecationWarning) + @skipif_ci_windows("crash") + @skipif_ci_apple("crash") def test_gdot3_svg(self): content = """ before diff --git a/sphinx_runpython/ext_test_case.py b/sphinx_runpython/ext_test_case.py index 762068c..3ad3bd3 100644 --- a/sphinx_runpython/ext_test_case.py +++ b/sphinx_runpython/ext_test_case.py @@ -108,6 +108,42 @@ def __exit__(self, exc_type, exc_value, traceback): sys.path = self.store +def is_windows() -> bool: + return sys.platform == "win32" + + +def is_apple() -> bool: + return sys.platform == "darwin" + + +def is_linux() -> bool: + return sys.platform == "linux" + + +def skipif_ci_windows(msg) -> Callable: + """Skips a unit test if it runs on :epkg:`azure pipeline` on :epkg:`Windows`.""" + if is_windows(): + msg = f"Test does not work on azure pipeline (Windows). {msg}" + return unittest.skip(msg) + return lambda x: x + + +def skipif_ci_linux(msg) -> Callable: + """Skips a unit test if it runs on :epkg:`azure pipeline` on :epkg:`Linux`.""" + if is_linux(): + msg = f"Takes too long (Linux). {msg}" + return unittest.skip(msg) + return lambda x: x + + +def skipif_ci_apple(msg) -> Callable: + """Skips a unit test if it runs on :epkg:`azure pipeline` on :epkg:`Windows`.""" + if is_apple(): + msg = f"Test does not work on azure pipeline (Apple). {msg}" + return unittest.skip(msg) + return lambda x: x + + class ExtTestCase(unittest.TestCase): _warns = []