4
4
from typing import Any
5
5
from typing import Dict
6
6
from typing import List
7
+ from typing import Optional
7
8
from typing import TYPE_CHECKING
8
9
9
10
import click
12
13
from _pytask .compat import import_optional_dependency
13
14
from _pytask .config import hookimpl
14
15
from _pytask .console import console
15
- from _pytask .dag import descending_tasks
16
16
from _pytask .exceptions import CollectionError
17
17
from _pytask .exceptions import ConfigurationError
18
18
from _pytask .exceptions import ResolvingDependenciesError
29
29
if TYPE_CHECKING :
30
30
from typing import NoReturn
31
31
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
+
32
39
33
40
@hookimpl (tryfirst = True )
34
41
def pytask_extend_command_line_interface (cli : click .Group ) -> None :
@@ -56,11 +63,33 @@ def pytask_parse_config(
56
63
key = "layout" ,
57
64
default = "dot" ,
58
65
)
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
59
88
60
89
61
90
_HELP_TEXT_LAYOUT : str = (
62
91
"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 ."
64
93
)
65
94
66
95
@@ -70,9 +99,20 @@ def pytask_parse_config(
70
99
)
71
100
72
101
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
+
73
108
@click .command ()
74
109
@click .option ("-l" , "--layout" , type = str , default = None , help = _HELP_TEXT_LAYOUT )
75
110
@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
+ )
76
116
def dag (** config_from_cli : Any ) -> "NoReturn" :
77
117
"""Create a visualization of the project's DAG."""
78
118
try :
@@ -181,10 +221,10 @@ def build_dag(config_from_cli: Dict[str, Any]) -> nx.DiGraph:
181
221
def _refine_dag (session : Session ) -> nx .DiGraph :
182
222
"""Refine the dag for plotting."""
183
223
dag = _shorten_node_labels (session .dag , session .config ["paths" ])
184
- dag = _add_root_node (dag )
185
224
dag = _clean_dag (dag )
186
225
dag = _style_dag (dag )
187
226
dag = _escape_node_names_with_colons (dag )
227
+ dag .graph ["graph" ] = {"rankdir" : session .config ["rank_direction" ]}
188
228
189
229
return dag
190
230
@@ -239,21 +279,6 @@ def _shorten_node_labels(dag: nx.DiGraph, paths: List[Path]) -> nx.DiGraph:
239
279
return dag
240
280
241
281
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
-
257
282
def _clean_dag (dag : nx .DiGraph ) -> nx .DiGraph :
258
283
"""Clean the DAG."""
259
284
for node in dag .nodes :
0 commit comments