Skip to content

Commit a7fd971

Browse files
authored
Refactor exporting the DAG as a figure. (#185)
1 parent a20c9b9 commit a7fd971

File tree

3 files changed

+46
-19
lines changed

3 files changed

+46
-19
lines changed

docs/source/changes.rst

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ all releases are available on `PyPI <https://pypi.org/project/pytask>`_ and
1010
0.1.5 - 2022-xx-xx
1111
------------------
1212

13+
- :gh:`185` fix issues with drawing a graph and adds the ``--rank-direction`` to change
14+
the direction of the DAG.
1315
- :gh:`186` enhance live displays by deactivating auto-refresh among other things.
1416
- :gh:`187` allows to enable and disable showing tracebacks and potentially different
1517
styles in the future with :confval:`show_traceback=True|False`.

src/_pytask/graph.py

+43-18
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import Any
55
from typing import Dict
66
from typing import List
7+
from typing import Optional
78
from typing import TYPE_CHECKING
89

910
import click
@@ -12,7 +13,6 @@
1213
from _pytask.compat import import_optional_dependency
1314
from _pytask.config import hookimpl
1415
from _pytask.console import console
15-
from _pytask.dag import descending_tasks
1616
from _pytask.exceptions import CollectionError
1717
from _pytask.exceptions import ConfigurationError
1818
from _pytask.exceptions import ResolvingDependenciesError
@@ -29,6 +29,13 @@
2929
if TYPE_CHECKING:
3030
from typing import NoReturn
3131

32+
if sys.version_info >= (3, 8):
33+
from typing import Literal
34+
else:
35+
from typing_extensions import Literal
36+
37+
_RankDirection = Literal["TB", "LR", "BT", "RL"]
38+
3239

3340
@hookimpl(tryfirst=True)
3441
def pytask_extend_command_line_interface(cli: click.Group) -> None:
@@ -56,11 +63,33 @@ def pytask_parse_config(
5663
key="layout",
5764
default="dot",
5865
)
66+
config["rank_direction"] = get_first_non_none_value(
67+
config_from_cli,
68+
config_from_file,
69+
key="rank_direction",
70+
default="TB",
71+
callback=_rank_direction_callback,
72+
)
73+
74+
75+
def _rank_direction_callback(
76+
x: Optional["_RankDirection"],
77+
) -> Optional["_RankDirection"]:
78+
"""Validate the passed options for rank direction."""
79+
if x in [None, "None", "none"]:
80+
x = None
81+
elif x in ["TB", "LR", "BT", "RL"]:
82+
pass
83+
else:
84+
raise ValueError(
85+
"'rank_direction' can only be one of ['TB', 'LR', 'BT', 'RL']."
86+
)
87+
return x
5988

6089

6190
_HELP_TEXT_LAYOUT: str = (
6291
"The layout determines the structure of the graph. Here you find an overview of "
63-
"all available layouts: https://graphviz.org/#roadmap."
92+
"all available layouts: https://graphviz.org/docs/layouts."
6493
)
6594

6695

@@ -70,9 +99,20 @@ def pytask_parse_config(
7099
)
71100

72101

102+
_HELP_TEXT_RANK_DIRECTION: str = (
103+
"The direction of the directed graph. It can be ordered from top to bottom, TB, "
104+
"left to right, LR, bottom to top, BT, or right to left, RL. [default: TB]"
105+
)
106+
107+
73108
@click.command()
74109
@click.option("-l", "--layout", type=str, default=None, help=_HELP_TEXT_LAYOUT)
75110
@click.option("-o", "--output-path", type=str, default=None, help=_HELP_TEXT_OUTPUT)
111+
@click.option(
112+
"--rank-direction",
113+
type=click.Choice(["TB", "LR", "BT", "RL"]),
114+
help=_HELP_TEXT_RANK_DIRECTION,
115+
)
76116
def dag(**config_from_cli: Any) -> "NoReturn":
77117
"""Create a visualization of the project's DAG."""
78118
try:
@@ -181,10 +221,10 @@ def build_dag(config_from_cli: Dict[str, Any]) -> nx.DiGraph:
181221
def _refine_dag(session: Session) -> nx.DiGraph:
182222
"""Refine the dag for plotting."""
183223
dag = _shorten_node_labels(session.dag, session.config["paths"])
184-
dag = _add_root_node(dag)
185224
dag = _clean_dag(dag)
186225
dag = _style_dag(dag)
187226
dag = _escape_node_names_with_colons(dag)
227+
dag.graph["graph"] = {"rankdir": session.config["rank_direction"]}
188228

189229
return dag
190230

@@ -239,21 +279,6 @@ def _shorten_node_labels(dag: nx.DiGraph, paths: List[Path]) -> nx.DiGraph:
239279
return dag
240280

241281

242-
def _add_root_node(dag: nx.DiGraph) -> nx.DiGraph:
243-
"""Add a root node to the graph to bind all starting nodes together."""
244-
tasks_without_predecessor = [
245-
name
246-
for name in dag.nodes
247-
if len(list(descending_tasks(name, dag))) == 0 and "task" in dag.nodes[name]
248-
]
249-
if tasks_without_predecessor:
250-
dag.add_node("root")
251-
for name in tasks_without_predecessor:
252-
dag.add_edge("root", name)
253-
254-
return dag
255-
256-
257282
def _clean_dag(dag: nx.DiGraph) -> nx.DiGraph:
258283
"""Clean the DAG."""
259284
for node in dag.nodes:

src/_pytask/logging.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ def _show_traceback_callback(
104104
elif x in ["no", "yes"]:
105105
pass
106106
else:
107-
raise ValueError("'show_traceback' can only be one of ['no', 'yes'")
107+
raise ValueError("'show_traceback' can only be one of ['no', 'yes'].")
108108
return x
109109

110110

0 commit comments

Comments
 (0)