├── .gitignore ├── legend.png ├── tasks.png ├── legend.dot ├── CHANGES ├── LICENSE ├── setup.py ├── README.md └── doit_graph.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.egg-info 3 | -------------------------------------------------------------------------------- /legend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydoit/doit-graph/HEAD/legend.png -------------------------------------------------------------------------------- /tasks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydoit/doit-graph/HEAD/tasks.png -------------------------------------------------------------------------------- /legend.dot: -------------------------------------------------------------------------------- 1 | digraph G { 2 | fontname="Helvetica"; 3 | labelloc=t; 4 | label="Legend"; 5 | 6 | node[style=filled, fontname="Helvetica", color=lightblue2]; 7 | edge[fontname="Helvetica"]; 8 | 9 | ta0[label="task A"] 10 | ta1[label="task B"] 11 | ta0->ta1 [label=" task-dep"]; 12 | 13 | tb0[label="task A"] 14 | tb1[label="task B"] 15 | tb0->tb1 [label=" setup task", arrowhead=empty]; 16 | 17 | tg[label="task-group", peripheries=2] 18 | 19 | } 20 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | 2 | ======= 3 | Changes 4 | ======= 5 | 6 | 7 | 0.3.0 (*2019-08-11*) 8 | ==================== 9 | 10 | - Support drawing tasks in left-to-right layout using `--horizontal` or `-h` option 11 | 12 | 13 | 0.2.0 (*2019-07-27*) 14 | ==================== 15 | 16 | - Support drawing tasks in execution order using `--reverse` option 17 | 18 | 19 | 0.1.1 (*2018-07-10*) 20 | ==================== 21 | 22 | - improve description given by `doit help graph` 23 | - print name of generated file 24 | 25 | 26 | 0.1.0 (*2018-07-07*) 27 | ==================== 28 | 29 | - initial release 30 | 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | 3 | The MIT License 4 | 5 | Copyright (c) 2018 Eduardo Naufel Schettino 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setup( 7 | name = 'doit-graph', 8 | description = "doit cmd plugin: create task's dependency-graph image", 9 | version = '0.3.0', 10 | license = 'MIT', 11 | author = 'Eduardo Naufel Schettino', 12 | url = 'http://github.com/pydoit/doit-graph', 13 | long_description=long_description, 14 | long_description_content_type="text/markdown", 15 | 16 | py_modules=['doit_graph'], 17 | install_requires = ['doit', 'pygraphviz'], 18 | entry_points = { 19 | 'doit.COMMAND': [ 20 | 'graph = doit_graph:GraphCmd' 21 | ] 22 | }, 23 | 24 | classifiers=( 25 | 'Development Status :: 5 - Production/Stable', 26 | 'Environment :: Console', 27 | 'License :: OSI Approved :: MIT License', 28 | 'Natural Language :: English', 29 | 'Operating System :: OS Independent', 30 | 'Operating System :: POSIX', 31 | 'Programming Language :: Python :: 3', 32 | 'Programming Language :: Python :: 3.5', 33 | 'Programming Language :: Python :: 3.6', 34 | 'Programming Language :: Python :: 3.7', 35 | 'Intended Audience :: Developers', 36 | ), 37 | keywords = "doit graph graphviz", 38 | ) 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # doit-graph 3 | 4 | Generates a graph (using graphviz's dot) of [doit](http://pydoit.org) tasks. 5 | 6 | Sample for [doit tutorial](http://pydoit.org/tutorial_1.html) tasks: 7 | 8 | ![Sample output](/tasks.png) 9 | 10 | 11 | ## install 12 | 13 | pip install doit-graph 14 | 15 | 16 | ## usage 17 | 18 | ``` 19 | $ doit graph 20 | $ dot -Tpng tasks.dot -o tasks.png 21 | ``` 22 | 23 | - By default sub-tasks are hidden. Use option `--show-subtasks` to display them. 24 | 25 | - By default all tasks are included in graph. 26 | It is possible to specify which tasks should be included in the graph (note dependencies will be automatically included). 27 | 28 | - To draw tasks in execution order (i.e. reverse of dependency direction), use option `--reverse` 29 | 30 | ``` 31 | $ doit graph --reverse 32 | ``` 33 | 34 | - To draw tasks from left-to-right instead of the default top-to-bottom, use option `--horizontal` or `-h` 35 | 36 | ``` 37 | $ doit graph --horizontal 38 | ``` 39 | 40 | ### legend 41 | 42 | ![Legend](/legend.png) 43 | 44 | - group-tasks have double bondary border in the node 45 | - `task-dep` arrow have a solid head 46 | - `setup-task` arrow have an empty head 47 | 48 | 49 | 50 | ### limitations 51 | 52 | `calc_dep` and `delayed-tasks` are not supported. 53 | 54 | 55 | 56 | ## DEV notes 57 | 58 | http://graphviz.org/doc/info/attrs.html 59 | -------------------------------------------------------------------------------- /doit_graph.py: -------------------------------------------------------------------------------- 1 | """doit-graph 2 | 3 | The MIT License 4 | 5 | Copyright (c) 2018-present Eduardo Naufel Schettino & contributors 6 | """ 7 | 8 | __version__ = (0, 3, 0) 9 | 10 | from collections import deque 11 | 12 | import pygraphviz 13 | 14 | from doit.cmd_base import DoitCmdBase 15 | from doit.control import TaskControl 16 | 17 | 18 | opt_subtasks = { 19 | 'name': 'subtasks', 20 | 'short': '', 21 | 'long': 'show-subtasks', 22 | 'type': bool, 23 | 'default': False, 24 | 'help': 'include subtasks in graph', 25 | } 26 | 27 | opt_reverse = { 28 | 'name': 'reverse', 29 | 'short': '', 30 | 'long': 'reverse', 31 | 'type': bool, 32 | 'default': False, 33 | 'help': 'draw edge in execution order, i.e. the reverse of dependency direction' 34 | } 35 | 36 | opt_horizontal = { 37 | 'name': 'horizontal', 38 | 'short': 'h', 39 | 'long': 'horizontal', 40 | 'type': bool, 41 | 'default': False, 42 | 'help': 'draw graph in left-right mode, i.e. add rankdir=LR to digraph output' 43 | } 44 | 45 | opt_outfile = { 46 | 'name': 'outfile', 47 | 'short': 'o', 48 | 'long': 'output', 49 | 'type': str, 50 | 'default': None, # actually default dependends parameters 51 | 'help': 'name of generated dot-file', 52 | } 53 | 54 | 55 | 56 | class GraphCmd(DoitCmdBase): 57 | name = 'graph' 58 | doc_purpose = "create task's dependency-graph (in dot file format)" 59 | doc_description = """Creates a DAG (directly acyclic graph) representation of tasks in graphviz's **dot** format (http://graphviz.org). 60 | 61 | **dot** files can be convert to images with i.e. 62 | 63 | $ dot -Tpng tasks.dot -o tasks.png 64 | 65 | Legend: 66 | - group-tasks have double boundary border in the node 67 | - `task-dep` arrow have a solid head 68 | - `setup-task` arrow have an empty head 69 | 70 | Website/docs: https://github.com/pydoit/doit-graph 71 | """ 72 | doc_usage = "[TASK ...]" 73 | 74 | cmd_options = (opt_subtasks, opt_outfile, opt_reverse, opt_horizontal) 75 | 76 | 77 | def node(self, task_name): 78 | """get graph node that should represent for task_name 79 | 80 | :param task_name: 81 | """ 82 | if self.subtasks: 83 | return task_name 84 | task = self.tasks[task_name] 85 | return task.subtask_of or task_name 86 | 87 | 88 | def add_edge(self, src_name, sink_name, arrowhead): 89 | source = self.node(src_name) 90 | sink = self.node(sink_name) 91 | if source != sink and (source, sink) not in self._edges: 92 | self._edges.add((source, sink)) 93 | self.graph.add_edge(source, sink, arrowhead=arrowhead) 94 | 95 | 96 | def _execute(self, subtasks, reverse, horizontal, outfile, pos_args=None): 97 | # init 98 | control = TaskControl(self.task_list) 99 | self.tasks = control.tasks 100 | self.subtasks = subtasks 101 | self._edges = set() # used to avoid adding same edge twice 102 | 103 | # create graph 104 | self.graph = pygraphviz.AGraph(strict=False, directed=True) 105 | self.graph.node_attr['color'] = 'lightblue2' 106 | self.graph.node_attr['style'] = 'filled' 107 | 108 | if (horizontal): 109 | self.graph.graph_attr.update(rankdir='LR') 110 | 111 | # populate graph 112 | processed = set() # str - task name 113 | if pos_args: 114 | to_process = deque(pos_args) 115 | else: 116 | to_process = deque(control.tasks.keys()) 117 | 118 | while to_process: 119 | task = control.tasks[to_process.popleft()] 120 | if task.name in processed: 121 | continue 122 | processed.add(task.name) 123 | 124 | # add nodes 125 | node_attrs = {} 126 | if task.has_subtask: 127 | node_attrs['peripheries'] = '2' 128 | if (not task.subtask_of) or subtasks: 129 | self.graph.add_node(task.name, **node_attrs) 130 | 131 | # add edges 132 | for sink_name in task.setup_tasks: 133 | self.add_edge(task.name, sink_name, arrowhead='empty') 134 | if sink_name not in processed: 135 | to_process.append(sink_name) 136 | for sink_name in task.task_dep: 137 | self.add_edge(task.name, sink_name, arrowhead='') 138 | if sink_name not in processed: 139 | to_process.append(sink_name) 140 | 141 | if not outfile: 142 | name = pos_args[0] if len(pos_args)==1 else 'tasks' 143 | outfile = '{}.dot'.format(name) 144 | print('Generated file: {}'.format(outfile)) 145 | if (reverse): 146 | self.graph.reverse().write(outfile) 147 | else: 148 | self.graph.write(outfile) 149 | 150 | --------------------------------------------------------------------------------