Python: Fix: Add meaningful error message when graphviz binary is not installed (#826)

* Initial plan

* Fix: Add meaningful error message for missing graphviz binary

Co-authored-by: ekzhu <320302+ekzhu@users.noreply.github.com>

* Fix CI error: Make graphviz test conditional to avoid import errors

Co-authored-by: ekzhu <320302+ekzhu@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: ekzhu <320302+ekzhu@users.noreply.github.com>
Co-authored-by: Eric Zhu <ekzhu@users.noreply.github.com>
This commit is contained in:
Copilot
2025-09-19 14:56:49 +09:00
committed by GitHub
Unverified
parent 367a3275d1
commit 965918b883
2 changed files with 52 additions and 16 deletions
@@ -89,25 +89,33 @@ class WorkflowViz:
dot_content = self.to_digraph()
source = graphviz.Source(dot_content)
if filename:
# Save to specified file
output_path = Path(filename)
if output_path.suffix and output_path.suffix[1:] != format:
raise ValueError(f"File extension {output_path.suffix} doesn't match format {format}")
try:
if filename:
# Save to specified file
output_path = Path(filename)
if output_path.suffix and output_path.suffix[1:] != format:
raise ValueError(f"File extension {output_path.suffix} doesn't match format {format}")
# Remove extension if present since graphviz.render() adds it
base_name = str(output_path.with_suffix(""))
source.render(base_name, format=format, cleanup=True)
# Return the actual filename with extension
return f"{base_name}.{format}"
# Create temporary file
with tempfile.NamedTemporaryFile(suffix=f".{format}", delete=False) as temp_file:
temp_path = Path(temp_file.name)
base_name = str(temp_path.with_suffix(""))
# Remove extension if present since graphviz.render() adds it
base_name = str(output_path.with_suffix(""))
source.render(base_name, format=format, cleanup=True)
# Return the actual filename with extension
return f"{base_name}.{format}"
# Create temporary file
with tempfile.NamedTemporaryFile(suffix=f".{format}", delete=False) as temp_file:
temp_path = Path(temp_file.name)
base_name = str(temp_path.with_suffix(""))
source.render(base_name, format=format, cleanup=True)
return f"{base_name}.{format}"
except graphviz.backend.execute.ExecutableNotFound as e:
raise ImportError(
"The graphviz executables are not found. The graphviz Python package is installed, but the "
"graphviz executables (dot, neato, etc.) are not available on your system's PATH. "
"Install graphviz executables: sudo apt-get install graphviz on Debian/Ubuntu, "
"brew install graphviz on macOS, or download from https://graphviz.org/download/ for other platforms."
) from e
def save_svg(self, filename: str) -> str:
"""Convenience method to save as SVG.
@@ -186,6 +186,34 @@ def test_workflow_viz_unsupported_format():
viz.export(format="invalid") # type: ignore
def test_workflow_viz_graphviz_binary_not_found():
"""Test that missing graphviz binary raises ImportError with helpful message."""
import unittest.mock
# Skip test if graphviz package is not available
pytest.importorskip("graphviz")
executor1 = MockExecutor(id="executor1")
executor2 = MockExecutor(id="executor2")
workflow = WorkflowBuilder().add_edge(executor1, executor2).set_start_executor(executor1).build()
viz = WorkflowViz(workflow)
# Mock graphviz.Source.render to raise ExecutableNotFound
with unittest.mock.patch("graphviz.Source") as mock_source_class:
mock_source = unittest.mock.MagicMock()
mock_source_class.return_value = mock_source
# Import the ExecutableNotFound exception for the test
from graphviz.backend.execute import ExecutableNotFound
mock_source.render.side_effect = ExecutableNotFound("failed to execute PosixPath('dot')")
# Test that the proper ImportError is raised with helpful message
with pytest.raises(ImportError, match="The graphviz executables are not found"):
viz.export(format="svg")
def test_workflow_viz_conditional_edge():
"""Test that conditional edges are rendered dashed with a label."""
start = MockExecutor(id="start")