From 6418d6a38752e861a6ff7547bb98c28915ca4a80 Mon Sep 17 00:00:00 2001 From: John Lapeyre Date: Fri, 10 Mar 2023 00:11:35 -0500 Subject: [PATCH 1/3] Get list of supported image formats from `dot` instead of hardcoding This PR replaces a list of image formats supported by graphviz that is hardcoded in rustworkx/visualization/graphviz.py with a list that is taken dynamically from the executable `dot`. With the version of `dot` that I tested, this list had 50 entries, while the hardcoded list had 35. The immediate reason for this PR is due to a failure in the test suite despite having a working `dot` on the PATH. Apparently `dot` can be compiled with varying support for image formats. In addition on some linux distributions, including some Ubuntu and some Fedora, the package `graphviz` does not support jpeg. But, installing the additional package `graphviz-devel` does add jpeg support. Before this PR, this would cause an untrapped error and a failure in the test suite, because only the presence of `dot` is checked, not which images it supports. With this PR, if your `dot` does not support `jpeg` then the test for writing a `jpeg` will be skipped. This PR also implements an error message that if both pillow and graphviz are missing says both are missing. Before, an error would be thrown with one message, then after installing the dependency, an error would be thrown with the second. --- rustworkx/visualization/__init__.py | 4 +- rustworkx/visualization/graphviz.py | 136 +++++++++--------- .../visualization/test_graphviz.py | 17 +-- 3 files changed, 76 insertions(+), 81 deletions(-) diff --git a/rustworkx/visualization/__init__.py b/rustworkx/visualization/__init__.py index 615932f9b..f2d91a5ca 100644 --- a/rustworkx/visualization/__init__.py +++ b/rustworkx/visualization/__init__.py @@ -11,7 +11,9 @@ __all__ = [ "mpl_draw", "graphviz_draw", + "have_dot", + "is_format_supported", ] from .matplotlib import mpl_draw -from .graphviz import graphviz_draw +from .graphviz import graphviz_draw, have_dot, is_format_supported diff --git a/rustworkx/visualization/graphviz.py b/rustworkx/visualization/graphviz.py index 4d5198814..2bc402d53 100644 --- a/rustworkx/visualization/graphviz.py +++ b/rustworkx/visualization/graphviz.py @@ -10,53 +10,62 @@ import tempfile import io +__all__ = ["graphviz_draw", "have_dot", "is_format_supported"] + +METHODS = {"twopi", "neato", "circo", "fdp", "sfdp", "dot"} + +_NO_PILLOW_MSG = """ +Pillow is necessary to use graphviz_draw() it can be installed +with 'pip install pydot pillow. +""" + +_NO_DOT_MSG = """ +Graphviz could not be found or run. This function requires that +Graphviz is installed. If you need to install Graphviz you can +refer to: https://graphviz.org/download/#executable-packages for +instructions. +""" + try: - from PIL import Image + import PIL + HAVE_PILLOW = True +except Exception: + HAVE_PILLOW = False - HAS_PILLOW = True -except ImportError: - HAS_PILLOW = False +# Return True if `dot` is found and executes. +def have_dot(): + try: + subprocess.run( + ["dot", "-V"], + cwd=tempfile.gettempdir(), + check=True, + capture_output=True, + ) + except Exception: + return False + return True -__all__ = ["graphviz_draw"] +def _capture_support_string(): + try: + res = subprocess.check_output( + ["dot", "-T", "bogus_format"], + stderr=subprocess.STDOUT, + ) + except subprocess.CalledProcessError as exerr: + return exerr.output.decode() -METHODS = {"twopi", "neato", "circo", "fdp", "sfdp", "dot"} -IMAGE_TYPES = { - "canon", - "cmap", - "cmapx", - "cmapx_np", - "dia", - "dot", - "fig", - "gd", - "gd2", - "gif", - "hpgl", - "imap", - "imap_np", - "ismap", - "jpe", - "jpeg", - "jpg", - "mif", - "mp", - "pcl", - "pdf", - "pic", - "plain", - "plain-ext", - "png", - "ps", - "ps2", - "svg", - "svgz", - "vml", - "vmlz" "vrml", - "vtx", - "wbmp", - "xdor", - "xlib", -} +# Return collection of image formats supported by dot, as +# a `set` of `str`. +def _supported_image_formats(): + error_string = _capture_support_string() + # 7 is a magic number based error message. + # The words following the first seven are the formats. + return set(error_string.split()[7:]) + + +def is_format_supported(image_format: str): + """Return true if `image_format` is supported by the installed graphviz.""" + return image_format in _supported_image_formats() def graphviz_draw( @@ -141,38 +150,27 @@ def node_attr(node): graphviz_draw(graph, node_attr_fn=node_attr, method='sfdp') """ - if not HAS_PILLOW: - raise ImportError( - "Pillow is necessary to use graphviz_draw() " - "it can be installed with 'pip install pydot pillow'" - ) - try: - subprocess.run( - ["dot", "-V"], - cwd=tempfile.gettempdir(), - check=True, - capture_output=True, - ) - except Exception: - raise RuntimeError( - "Graphviz could not be found or run. This function requires that " - "Graphviz is installed. If you need to install Graphviz you can " - "refer to: https://graphviz.org/download/#executable-packages for " - "instructions." - ) + _have_dot = have_dot() + if not (HAVE_PILLOW and _have_dot): + raise RuntimeError(_NO_DOT_MSG + _NO_PILLOW_MSG) + if not HAVE_PILLOW: + raise ImportError(_NO_PILLOW_MSG) + if not _have_dot: + raise RuntimeError(_NO_DOT_MSG) dot_str = graph.to_dot(node_attr_fn, edge_attr_fn, graph_attr) if image_type is None: output_format = "png" else: - if image_type not in IMAGE_TYPES: - raise ValueError( - "The specified value for the image_type argument, " - f"'{image_type}' is not a valid choice. It must be one of: " - f"{IMAGE_TYPES}" - ) output_format = image_type + if output_format not in (supported_formats := _supported_image_formats()): + raise ValueError( + "The specified value for the image_type argument, " + f"'{output_format}' is not a valid choice. It must be one of: " + f"{supported_formats}" + ) + if method is None: prog = "dot" else: @@ -193,7 +191,7 @@ def node_attr(node): text=False, ) dot_bytes_image = io.BytesIO(dot_result.stdout) - image = Image.open(dot_bytes_image) + image = PIL.Image.open(dot_bytes_image) return image else: subprocess.run( diff --git a/tests/rustworkx_tests/visualization/test_graphviz.py b/tests/rustworkx_tests/visualization/test_graphviz.py index af2733141..ae291ed63 100644 --- a/tests/rustworkx_tests/visualization/test_graphviz.py +++ b/tests/rustworkx_tests/visualization/test_graphviz.py @@ -16,20 +16,13 @@ import unittest import rustworkx -from rustworkx.visualization import graphviz_draw +from rustworkx.visualization import graphviz_draw, have_dot, is_format_supported try: import PIL - - subprocess.run( - ["dot", "-V"], - cwd=tempfile.gettempdir(), - check=True, - capture_output=True, - ) - HAS_PILLOW = True + HAVE_PILLOW = True except Exception: - HAS_PILLOW = False + HAVE_PILLOW = False SAVE_IMAGES = os.getenv("RETWORKX_TEST_PRESERVE_IMAGES", None) @@ -39,7 +32,7 @@ def _save_image(image, path): image.save(path) -@unittest.skipUnless(HAS_PILLOW, "pillow and graphviz are required for running these tests") +@unittest.skipUnless(HAVE_PILLOW and have_dot(), "pillow and graphviz are required for running these tests") class TestGraphvizDraw(unittest.TestCase): def test_draw_no_args(self): graph = rustworkx.generators.star_graph(24) @@ -117,6 +110,8 @@ def test_draw_graph_attr(self): self.assertIsInstance(image, PIL.Image.Image) _save_image(image, "test_graphviz_draw_graph_attr.png") + + @unittest.skipUnless(is_format_supported("jpg"), "Installed graphviz does not support jpg image format.") def test_image_type(self): graph = rustworkx.directed_gnp_random_graph(50, 0.8) image = graphviz_draw(graph, image_type="jpg") From b6893f19a6cdcf4ec33eea3d7b63b6e8e802b1ac Mon Sep 17 00:00:00 2001 From: John Lapeyre Date: Fri, 10 Mar 2023 01:05:12 -0500 Subject: [PATCH 2/3] Fix lint complaints --- rustworkx/visualization/graphviz.py | 7 ++++++- tests/rustworkx_tests/visualization/test_graphviz.py | 12 +++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/rustworkx/visualization/graphviz.py b/rustworkx/visualization/graphviz.py index 2bc402d53..7f8dd1d24 100644 --- a/rustworkx/visualization/graphviz.py +++ b/rustworkx/visualization/graphviz.py @@ -28,10 +28,12 @@ try: import PIL + HAVE_PILLOW = True except Exception: HAVE_PILLOW = False + # Return True if `dot` is found and executes. def have_dot(): try: @@ -45,15 +47,18 @@ def have_dot(): return False return True + def _capture_support_string(): try: - res = subprocess.check_output( + subprocess.check_output( ["dot", "-T", "bogus_format"], + cwd=tempfile.gettempdir(), stderr=subprocess.STDOUT, ) except subprocess.CalledProcessError as exerr: return exerr.output.decode() + # Return collection of image formats supported by dot, as # a `set` of `str`. def _supported_image_formats(): diff --git a/tests/rustworkx_tests/visualization/test_graphviz.py b/tests/rustworkx_tests/visualization/test_graphviz.py index ae291ed63..1152eed62 100644 --- a/tests/rustworkx_tests/visualization/test_graphviz.py +++ b/tests/rustworkx_tests/visualization/test_graphviz.py @@ -11,8 +11,6 @@ # under the License. import os -import subprocess -import tempfile import unittest import rustworkx @@ -20,6 +18,7 @@ try: import PIL + HAVE_PILLOW = True except Exception: HAVE_PILLOW = False @@ -32,7 +31,9 @@ def _save_image(image, path): image.save(path) -@unittest.skipUnless(HAVE_PILLOW and have_dot(), "pillow and graphviz are required for running these tests") +@unittest.skipUnless( + HAVE_PILLOW and have_dot(), "pillow and graphviz are required for running these tests" +) class TestGraphvizDraw(unittest.TestCase): def test_draw_no_args(self): graph = rustworkx.generators.star_graph(24) @@ -110,8 +111,9 @@ def test_draw_graph_attr(self): self.assertIsInstance(image, PIL.Image.Image) _save_image(image, "test_graphviz_draw_graph_attr.png") - - @unittest.skipUnless(is_format_supported("jpg"), "Installed graphviz does not support jpg image format.") + @unittest.skipUnless( + is_format_supported("jpg"), "Installed graphviz does not support jpg image format." + ) def test_image_type(self): graph = rustworkx.directed_gnp_random_graph(50, 0.8) image = graphviz_draw(graph, image_type="jpg") From 1224839321d98d9c4f839d7e29c3419f46ef7bd7 Mon Sep 17 00:00:00 2001 From: John Lapeyre Date: Fri, 10 Mar 2023 11:23:52 -0500 Subject: [PATCH 3/3] Make code Python 3.7 compatible --- rustworkx/visualization/graphviz.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rustworkx/visualization/graphviz.py b/rustworkx/visualization/graphviz.py index 7f8dd1d24..8205f58a7 100644 --- a/rustworkx/visualization/graphviz.py +++ b/rustworkx/visualization/graphviz.py @@ -169,7 +169,8 @@ def node_attr(node): else: output_format = image_type - if output_format not in (supported_formats := _supported_image_formats()): + supported_formats = _supported_image_formats() + if output_format not in supported_formats: raise ValueError( "The specified value for the image_type argument, " f"'{output_format}' is not a valid choice. It must be one of: "