Skip to content

Commit c3948db

Browse files
authored
Replace pydot with pygraphviz. (#308)
1 parent e3c1a4c commit c3948db

File tree

6 files changed

+38
-74
lines changed

6 files changed

+38
-74
lines changed

.github/workflows/main.yml

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -65,23 +65,3 @@ jobs:
6565
if: runner.os == 'Linux' && matrix.python-version == '3.8'
6666
shell: bash -l {0}
6767
run: bash <(curl -s https://codecov.io/bash) -F end_to_end -c
68-
69-
70-
docs:
71-
72-
name: Run documentation.
73-
runs-on: ubuntu-latest
74-
75-
steps:
76-
- uses: actions/checkout@v2
77-
- uses: conda-incubator/setup-miniconda@v2
78-
with:
79-
auto-update-conda: true
80-
81-
- name: Install core dependencies.
82-
shell: bash -l {0}
83-
run: conda install -c conda-forge tox-conda
84-
85-
- name: Build docs
86-
shell: bash -l {0}
87-
run: tox -e sphinx

docs/source/tutorials/visualizing_the_dag.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
# Visualizing the DAG
22

33
To visualize the {term}`DAG` of the project, first, install
4-
[pydot](https://github.com/pydot/pydot) and [graphviz](https://graphviz.org/). For
5-
example, you can both install with conda
4+
[pygraphviz](https://github.com/pygraphviz/pygraphviz) and
5+
[graphviz](https://graphviz.org/). For example, you can both install with conda
66

77
```console
8-
$ conda install -c conda-forge pydot
8+
$ conda install -c conda-forge pygraphviz
99
```
1010

1111
After that, pytask offers two interfaces to visualize your project's {term}`DAG`.
@@ -27,7 +27,7 @@ There are ways to customize the visualization.
2727
other layouts, which are listed [here](https://graphviz.org/docs/layouts/).
2828
1. Using the {option}`pytask dag --output-path` option, you can provide a file name for
2929
the graph. The file extension changes the output format as supported by
30-
[pydot](https://github.com/pydot/pydot).
30+
[pygraphviz](https://github.com/pygraphviz/pygraphviz).
3131

3232
## Programmatic Interface
3333

@@ -49,10 +49,10 @@ shape of all nodes to hexagons by adding the property to the node attributes.
4949
nx.set_node_attributes(dag, "hexagon", "shape")
5050
```
5151

52-
For drawing, you better switch to pydot or pygraphviz since the matplotlib backend
53-
handles shapes with texts poorly. Here we use pydot and store the graph as a `.svg`.
52+
For drawing, you better switch to pygraphviz since the matplotlib backend handles shapes
53+
with texts poorly. Here we store the graph as a `.svg`.
5454

5555
```python
56-
graph = nx.nx_pydot.to_pydot(dag)
57-
graph.write_svg(produces)
56+
graph = nx.nx_agraph.to_agraph(dag)
57+
graph.draw(path, prog=layout)
5858
```

environment.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ dependencies:
3232
- jupyterlab
3333
- matplotlib
3434
- pre-commit
35-
- pydot
35+
- pygraphviz
3636
- pytest
3737
- pytest-cov
3838
- pytest-xdist

src/_pytask/graph.py

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ def dag(**config_from_cli: Any) -> NoReturn:
123123
else:
124124
try:
125125
session.hook.pytask_log_session_header(session=session)
126-
import_optional_dependency("pydot")
126+
import_optional_dependency("pygraphviz")
127127
check_for_optional_program(
128128
session.config["layout"],
129129
extra="The layout program is part of the graphviz package which you "
@@ -154,10 +154,11 @@ def build_dag(config_from_cli: dict[str, Any]) -> nx.DiGraph:
154154
"""Build the DAG.
155155
156156
This function is the programmatic interface to ``pytask dag`` and returns a
157-
preprocessed :class:`pydot.Dot` which makes plotting easier than with matplotlib.
157+
preprocessed :class:`pygraphviz.AGraph` which makes plotting easier than with
158+
matplotlib.
158159
159160
To change the style of the graph, it might be easier to convert the graph back to
160-
networkx, set attributes, and convert back to pydot or pygraphviz.
161+
networkx, set attributes, and convert back to pygraphviz.
161162
162163
Parameters
163164
----------
@@ -167,7 +168,7 @@ def build_dag(config_from_cli: dict[str, Any]) -> nx.DiGraph:
167168
168169
Returns
169170
-------
170-
pydot.Dot
171+
pygraphviz.AGraph
171172
A preprocessed graph which can be customized and exported.
172173
173174
"""
@@ -190,7 +191,7 @@ def build_dag(config_from_cli: dict[str, Any]) -> nx.DiGraph:
190191
else:
191192
try:
192193
session.hook.pytask_log_session_header(session=session)
193-
import_optional_dependency("pydot")
194+
import_optional_dependency("pygraphviz")
194195
check_for_optional_program(
195196
session.config["layout"],
196197
extra="The layout program is part of the graphviz package which you "
@@ -212,7 +213,6 @@ def _refine_dag(session: Session) -> nx.DiGraph:
212213
dag = _shorten_node_labels(session.dag, session.config["paths"])
213214
dag = _clean_dag(dag)
214215
dag = _style_dag(dag)
215-
dag = _escape_node_names_with_colons(dag)
216216
dag.graph["graph"] = {"rankdir": session.config["rank_direction"].name}
217217

218218
return dag
@@ -239,7 +239,7 @@ def _create_session(config_from_cli: dict[str, Any]) -> nx.DiGraph:
239239
else:
240240
try:
241241
session.hook.pytask_log_session_header(session=session)
242-
import_optional_dependency("pydot")
242+
import_optional_dependency("pygraphviz")
243243
check_for_optional_program(session.config["layout"])
244244
session.hook.pytask_collect(session=session)
245245
session.hook.pytask_resolve_dependencies(session=session)
@@ -282,19 +282,8 @@ def _style_dag(dag: nx.DiGraph) -> nx.DiGraph:
282282
return dag
283283

284284

285-
def _escape_node_names_with_colons(dag: nx.DiGraph) -> nx.DiGraph:
286-
"""Escape node names with colons.
287-
288-
pydot cannot handle colons in node names since it messes up some syntax. Escaping
289-
works by wrapping the string in double quotes. See this issue for more information:
290-
https://github.com/pydot/pydot/issues/224.
291-
292-
"""
293-
return nx.relabel_nodes(dag, {name: f'"{name}"' for name in dag.nodes})
294-
295-
296285
def _write_graph(dag: nx.DiGraph, path: Path, layout: str) -> None:
297286
"""Write the graph to disk."""
298287
path.parent.mkdir(exist_ok=True, parents=True)
299-
graph = nx.nx_pydot.to_pydot(dag)
300-
graph.write(path, prog=layout, format=path.suffix[1:])
288+
graph = nx.nx_agraph.to_agraph(dag)
289+
graph.draw(path, prog=layout)

tests/test_graph.py

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
from pytask import ExitCode
1111

1212
try:
13-
import pydot # noqa: F401
13+
import pygraphviz # noqa: F401
1414
except ImportError: # pragma: no cover
15-
_IS_PYDOT_INSTALLED = False
15+
_IS_PYGRAPHVIZ_INSTALLED = False
1616
else:
17-
_IS_PYDOT_INSTALLED = True
17+
_IS_PYGRAPHVIZ_INSTALLED = True
1818

1919
_GRAPH_LAYOUTS = ["neato", "dot", "fdp", "sfdp", "twopi", "circo"]
2020

@@ -34,7 +34,7 @@
3434

3535

3636
@pytest.mark.end_to_end
37-
@pytest.mark.skipif(not _IS_PYDOT_INSTALLED, reason="pydot is required")
37+
@pytest.mark.skipif(not _IS_PYGRAPHVIZ_INSTALLED, reason="pygraphviz is required")
3838
@pytest.mark.parametrize("layout", _PARAMETRIZED_LAYOUTS)
3939
@pytest.mark.parametrize("format_", _TEST_FORMATS)
4040
@pytest.mark.parametrize("rankdir", ["LR"])
@@ -70,7 +70,7 @@ def task_example(): pass
7070

7171

7272
@pytest.mark.end_to_end
73-
@pytest.mark.skipif(not _IS_PYDOT_INSTALLED, reason="pydot is required")
73+
@pytest.mark.skipif(not _IS_PYGRAPHVIZ_INSTALLED, reason="pygraphviz is required")
7474
@pytest.mark.parametrize("layout", _PARAMETRIZED_LAYOUTS)
7575
@pytest.mark.parametrize("format_", _TEST_FORMATS)
7676
@pytest.mark.parametrize("rankdir", [_RankDirection.LR.value, _RankDirection.TB])
@@ -91,9 +91,9 @@ def task_example(): pass
9191
def task_create_graph():
9292
dag = pytask.build_dag({{"paths": Path(__file__).parent}})
9393
dag.graph = {{"rankdir": "{rankdir_str}"}}
94-
graph = nx.nx_pydot.to_pydot(dag)
94+
graph = nx.nx_agraph.to_agraph(dag)
9595
path = Path(__file__).parent.joinpath("dag.{format_}")
96-
graph.write(path, prog="{layout}", format=path.suffix[1:])
96+
graph.draw(path, prog="{layout}")
9797
"""
9898

9999
tmp_path.joinpath("task_example.py").write_text(textwrap.dedent(source))
@@ -124,7 +124,7 @@ def task_example(): pass
124124

125125
monkeypatch.setattr(
126126
"_pytask.compat.importlib.import_module",
127-
lambda x: _raise_exc(ImportError("pydot not found")), # noqa: U100
127+
lambda x: _raise_exc(ImportError("pygraphviz not found")), # noqa: U100
128128
)
129129

130130
result = runner.invoke(
@@ -133,8 +133,9 @@ def task_example(): pass
133133
)
134134

135135
assert result.exit_code == ExitCode.FAILED
136-
assert "pytask requires the optional dependency 'pydot'." in result.output
137-
assert "pip or conda" in result.output
136+
assert "pytask requires the optional dependency 'pygraphviz'." in result.output
137+
assert "pip" in result.output
138+
assert "conda" in result.output
138139
assert "Traceback" not in result.output
139140
assert not tmp_path.joinpath("dag.png").exists()
140141

@@ -150,22 +151,23 @@ def test_raise_error_with_graph_via_task_missing_optional_dependency(
150151
151152
def task_create_graph():
152153
dag = pytask.build_dag({"paths": Path(__file__).parent})
153-
graph = nx.nx_pydot.to_pydot(dag)
154+
graph = nx.nx_agraph.to_agraph(dag)
154155
path = Path(__file__).parent.joinpath("dag.png")
155-
graph.write(path, prog="dot", format=path.suffix[1:])
156+
graph.draw(path, prog="dot")
156157
"""
157158
tmp_path.joinpath("task_example.py").write_text(textwrap.dedent(source))
158159

159160
monkeypatch.setattr(
160161
"_pytask.compat.importlib.import_module",
161-
lambda x: _raise_exc(ImportError("pydot not found")), # noqa: U100
162+
lambda x: _raise_exc(ImportError("pygraphviz not found")), # noqa: U100
162163
)
163164

164165
result = runner.invoke(cli, [tmp_path.as_posix()])
165166

166167
assert result.exit_code == ExitCode.FAILED
167-
assert "pytask requires the optional dependency 'pydot'." in result.output
168-
assert "pip or conda" in result.output
168+
assert "pytask requires the optional dependency 'pygraphviz'." in result.output
169+
assert "pip" in result.output
170+
assert "conda" in result.output
169171
assert "Traceback" in result.output
170172
assert not tmp_path.joinpath("dag.png").exists()
171173

@@ -211,9 +213,9 @@ def test_raise_error_with_graph_via_task_missing_optional_program(
211213
212214
def task_create_graph():
213215
dag = pytask.build_dag({"paths": Path(__file__).parent})
214-
graph = nx.nx_pydot.to_pydot(dag)
216+
graph = nx.nx_agraph.to_agraph(dag)
215217
path = Path(__file__).parent.joinpath("dag.png")
216-
graph.write(path, prog="dot", format=path.suffix[1:])
218+
graph.draw(path, prog="dot")
217219
"""
218220
tmp_path.joinpath("task_example.py").write_text(textwrap.dedent(source))
219221

tox.ini

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ conda_deps =
3030
# Optional and test dependencies
3131
graphviz
3232
pexpect
33-
pydot
33+
pygraphviz
3434

3535
commands =
3636
pytest {posargs}
@@ -42,12 +42,6 @@ commands =
4242
sphinx-build -T -b html -d {envtmpdir}/doctrees . {envtmpdir}/html
4343
- sphinx-build -T -b linkcheck -d {envtmpdir}/doctrees . {envtmpdir}/linkcheck
4444

45-
[doc8]
46-
ignore = D002, D004
47-
ignore-path =
48-
docs/build
49-
max-line-length = 88
50-
5145
[flake8]
5246
docstring-convention = numpy
5347
exclude =
@@ -75,7 +69,6 @@ filterwarnings =
7569
ignore: the imp module is deprecated in favour of importlib
7670
ignore: Using or importing the ABCs from 'collections' instead of from
7771
ignore: The (parser|symbol) module is deprecated and will be removed in future
78-
ignore: nx\.nx_pydot\.to_pydot depends on the pydot package
7972
markers =
8073
wip: Tests that are work-in-progress.
8174
unit: Flag for unit tests which target mainly a single function.

0 commit comments

Comments
 (0)