-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add option to save graph as PNG #523
Changes from 5 commits
5451254
32b21e1
9bbae3f
1b3fbe3
e439e64
7e1877b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -42,13 +42,15 @@ class ExecutorWithDependencies(ExecutorBase): | |||||||||||||||||||||||||||
Args: | ||||||||||||||||||||||||||||
refresh_rate (float, optional): The refresh rate for updating the executor queue. Defaults to 0.01. | ||||||||||||||||||||||||||||
plot_dependency_graph (bool, optional): Whether to generate and plot the dependency graph. Defaults to False. | ||||||||||||||||||||||||||||
plot_dependency_graph_filename (str): Name of the file to store the plotted graph in. | ||||||||||||||||||||||||||||
*args: Variable length argument list. | ||||||||||||||||||||||||||||
**kwargs: Arbitrary keyword arguments. | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
Attributes: | ||||||||||||||||||||||||||||
_future_hash_dict (Dict[str, Future]): A dictionary mapping task hash to future object. | ||||||||||||||||||||||||||||
_task_hash_dict (Dict[str, Dict]): A dictionary mapping task hash to task dictionary. | ||||||||||||||||||||||||||||
_generate_dependency_graph (bool): Whether to generate the dependency graph. | ||||||||||||||||||||||||||||
_generate_dependency_graph (str): Name of the file to store the plotted graph in. | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
|
@@ -57,6 +59,7 @@ def __init__( | |||||||||||||||||||||||||||
*args: Any, | ||||||||||||||||||||||||||||
refresh_rate: float = 0.01, | ||||||||||||||||||||||||||||
plot_dependency_graph: bool = False, | ||||||||||||||||||||||||||||
plot_dependency_graph_filename: Optional[str] = None, | ||||||||||||||||||||||||||||
**kwargs: Any, | ||||||||||||||||||||||||||||
) -> None: | ||||||||||||||||||||||||||||
super().__init__(max_cores=kwargs.get("max_cores", None)) | ||||||||||||||||||||||||||||
|
@@ -75,7 +78,11 @@ def __init__( | |||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||
self._future_hash_dict = {} | ||||||||||||||||||||||||||||
self._task_hash_dict = {} | ||||||||||||||||||||||||||||
self._generate_dependency_graph = plot_dependency_graph | ||||||||||||||||||||||||||||
self._plot_dependency_graph_filename = plot_dependency_graph_filename | ||||||||||||||||||||||||||||
if plot_dependency_graph_filename is None: | ||||||||||||||||||||||||||||
self._generate_dependency_graph = plot_dependency_graph | ||||||||||||||||||||||||||||
else: | ||||||||||||||||||||||||||||
self._generate_dependency_graph = True | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
def submit( | ||||||||||||||||||||||||||||
self, | ||||||||||||||||||||||||||||
|
@@ -142,7 +149,11 @@ def __exit__( | |||||||||||||||||||||||||||
v: k for k, v in self._future_hash_dict.items() | ||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||
return draw(node_lst=node_lst, edge_lst=edge_lst) | ||||||||||||||||||||||||||||
return draw( | ||||||||||||||||||||||||||||
node_lst=node_lst, | ||||||||||||||||||||||||||||
edge_lst=edge_lst, | ||||||||||||||||||||||||||||
filename=self._plot_dependency_graph_filename, | ||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||
Comment on lines
+152
to
+156
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add error handling for file operations. The - return draw(
- node_lst=node_lst,
- edge_lst=edge_lst,
- filename=self._plot_dependency_graph_filename,
- )
+ try:
+ return draw(
+ node_lst=node_lst,
+ edge_lst=edge_lst,
+ filename=self._plot_dependency_graph_filename,
+ )
+ except IOError as e:
+ raise IOError(f"Failed to save dependency graph: {e}") from e 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
def create_executor( | ||||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,5 +1,6 @@ | ||||||||||||||||||||||||||||||||
import os.path | ||||||||||||||||||||||||||||||||
from concurrent.futures import Future | ||||||||||||||||||||||||||||||||
from typing import Tuple | ||||||||||||||||||||||||||||||||
from typing import Optional, Tuple | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
import cloudpickle | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
@@ -106,23 +107,27 @@ def convert_arg(arg, future_hash_inverse_dict): | |||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
def draw(node_lst: list, edge_lst: list): | ||||||||||||||||||||||||||||||||
def draw(node_lst: list, edge_lst: list, filename: Optional[str] = None): | ||||||||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||||||||
Draw the graph visualization of nodes and edges. | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
Args: | ||||||||||||||||||||||||||||||||
node_lst (list): List of nodes. | ||||||||||||||||||||||||||||||||
edge_lst (list): List of edges. | ||||||||||||||||||||||||||||||||
filename (str): Name of the file to store the plotted graph in. | ||||||||||||||||||||||||||||||||
""" | ||||||||||||||||||||||||||||||||
from IPython.display import SVG, display # noqa | ||||||||||||||||||||||||||||||||
import matplotlib.pyplot as plt # noqa | ||||||||||||||||||||||||||||||||
import networkx as nx # noqa | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
graph = nx.DiGraph() | ||||||||||||||||||||||||||||||||
for node in node_lst: | ||||||||||||||||||||||||||||||||
graph.add_node(node["id"], label=node["name"], shape=node["shape"]) | ||||||||||||||||||||||||||||||||
for edge in edge_lst: | ||||||||||||||||||||||||||||||||
graph.add_edge(edge["start"], edge["end"], label=edge["label"]) | ||||||||||||||||||||||||||||||||
svg = nx.nx_agraph.to_agraph(graph).draw(prog="dot", format="svg") | ||||||||||||||||||||||||||||||||
display(SVG(svg)) | ||||||||||||||||||||||||||||||||
plt.show() | ||||||||||||||||||||||||||||||||
if filename is not None: | ||||||||||||||||||||||||||||||||
file_format = os.path.splitext(filename)[-1][1:] | ||||||||||||||||||||||||||||||||
with open(filename, "wb") as f: | ||||||||||||||||||||||||||||||||
f.write(nx.nx_agraph.to_agraph(graph).draw(prog="dot", format=file_format)) | ||||||||||||||||||||||||||||||||
Comment on lines
+126
to
+129
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add error handling for file operations The file operations should include error handling for common issues like permission errors or invalid paths. if filename is not None:
file_format = os.path.splitext(filename)[-1][1:]
+ if not file_format:
+ raise ValueError("Filename must have an extension (e.g., .png, .svg, .pdf)")
+ if file_format not in ['png', 'svg', 'pdf']:
+ raise ValueError(f"Unsupported file format: {file_format}")
+ try:
with open(filename, "wb") as f:
f.write(nx.nx_agraph.to_agraph(graph).draw(prog="dot", format=file_format))
+ except (OSError, IOError) as e:
+ raise IOError(f"Failed to save graph to {filename}: {str(e)}") 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||
else: | ||||||||||||||||||||||||||||||||
from IPython.display import SVG, display # noqa | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
display(SVG(nx.nx_agraph.to_agraph(graph).draw(prog="dot", format="svg"))) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
from concurrent.futures import Future | ||
import os | ||
import unittest | ||
from time import sleep | ||
from queue import Queue | ||
|
@@ -73,6 +74,26 @@ def test_executor_dependency_plot(self): | |
self.assertEqual(len(nodes), 5) | ||
self.assertEqual(len(edges), 4) | ||
|
||
@unittest.skipIf( | ||
skip_graphviz_test, | ||
"graphviz is not installed, so the plot_dependency_graph tests are skipped.", | ||
) | ||
def test_executor_dependency_plot_filename(self): | ||
graph_file = os.path.join(os.path.dirname(__file__), "test.png") | ||
with Executor( | ||
max_cores=1, | ||
backend="local", | ||
plot_dependency_graph=False, | ||
plot_dependency_graph_filename=graph_file, | ||
) as exe: | ||
cloudpickle_register(ind=1) | ||
future_1 = exe.submit(add_function, 1, parameter_2=2) | ||
future_2 = exe.submit(add_function, 1, parameter_2=future_1) | ||
self.assertTrue(future_1.done()) | ||
self.assertTrue(future_2.done()) | ||
self.assertTrue(os.path.exists(graph_file)) | ||
# os.remove(graph_file) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Enhance test coverage for graph file output The test case needs improvements:
def test_executor_dependency_plot_filename(self):
- graph_file = os.path.join(os.path.dirname(__file__), "test.png")
- with Executor(
- max_cores=1,
- backend="local",
- plot_dependency_graph=False,
- plot_dependency_graph_filename=graph_file,
- ) as exe:
- cloudpickle_register(ind=1)
- future_1 = exe.submit(add_function, 1, parameter_2=2)
- future_2 = exe.submit(add_function, 1, parameter_2=future_1)
- self.assertTrue(future_1.done())
- self.assertTrue(future_2.done())
- self.assertTrue(os.path.exists(graph_file))
- # os.remove(graph_file)
+ test_files = {
+ 'png': os.path.join(os.path.dirname(__file__), "test.png"),
+ 'svg': os.path.join(os.path.dirname(__file__), "test.svg"),
+ 'pdf': os.path.join(os.path.dirname(__file__), "test.pdf")
+ }
+
+ try:
+ # Test valid formats
+ for fmt, graph_file in test_files.items():
+ with Executor(
+ max_cores=1,
+ backend="local",
+ plot_dependency_graph=False,
+ plot_dependency_graph_filename=graph_file,
+ ) as exe:
+ cloudpickle_register(ind=1)
+ future_1 = exe.submit(add_function, 1, parameter_2=2)
+ future_2 = exe.submit(add_function, 1, parameter_2=future_1)
+ self.assertTrue(future_1.done())
+ self.assertTrue(future_2.done())
+ self.assertTrue(os.path.exists(graph_file))
+
+ # Test invalid format
+ with self.assertRaises(ValueError):
+ with Executor(
+ max_cores=1,
+ backend="local",
+ plot_dependency_graph_filename="test.invalid"
+ ) as exe:
+ future_1 = exe.submit(add_function, 1, parameter_2=2)
+
+ # Test invalid path
+ with self.assertRaises(IOError):
+ with Executor(
+ max_cores=1,
+ backend="local",
+ plot_dependency_graph_filename="/invalid/path/test.png"
+ ) as exe:
+ future_1 = exe.submit(add_function, 1, parameter_2=2)
+
+ finally:
+ # Cleanup
+ for graph_file in test_files.values():
+ if os.path.exists(graph_file):
+ os.remove(graph_file)
|
||
def test_create_executor_error(self): | ||
with self.assertRaises(ValueError): | ||
create_executor(backend="toast", resource_dict={"cores": 1}) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add input validation for the filename parameter.
The
plot_dependency_graph_filename
parameter should be validated before being passed to_ExecutorWithDependencies
.Add a validation check similar to other parameters:
Then use it before the
_ExecutorWithDependencies
instantiation:+ _check_plot_dependency_graph_filename(plot_dependency_graph_filename) return _ExecutorWithDependencies( max_workers=max_workers, ...