1from collections import OrderedDict
2
3from .control import TaskControl
4from .cmd_base import DoitCmdBase
5from .cmd_base import check_tasks_exist
6
7
8opt_clean_dryrun = {
9    'name': 'dryrun',
10    'short': 'n', # like make dry-run
11    'long': 'dry-run',
12    'type': bool,
13    'default': False,
14    'help': 'print actions without really executing them',
15    }
16
17opt_clean_cleandep = {
18    'name': 'cleandep',
19    'short': 'c', # clean
20    'long': 'clean-dep',
21    'type': bool,
22    'default': False,
23    'help': 'clean task dependencies too',
24    }
25
26opt_clean_cleanall = {
27    'name': 'cleanall',
28    'short': 'a', # all
29    'long': 'clean-all',
30    'type': bool,
31    'default': False,
32    'help': 'clean all task',
33    }
34
35opt_clean_forget = {
36    'name': 'cleanforget',
37    'long': 'forget',
38    'type': bool,
39    'default': False,
40    'help': 'also forget tasks after cleaning',
41    }
42
43class Clean(DoitCmdBase):
44    doc_purpose = "clean action / remove targets"
45    doc_usage = "[TASK ...]"
46    doc_description = ("If no task is specified clean default tasks and "
47                       "set --clean-dep automatically.")
48
49    cmd_options = (opt_clean_cleandep, opt_clean_cleanall,
50                   opt_clean_dryrun, opt_clean_forget)
51
52
53    def clean_tasks(self, tasks, dryrun, cleanforget):
54        """ensure task clean-action is executed only once"""
55        cleaned = set()
56        forget_tasks = cleanforget and not dryrun
57        for task in tasks:
58            if task.name not in cleaned:
59                cleaned.add(task.name)
60                task.clean(self.outstream, dryrun)
61                if forget_tasks:
62                    self.dep_manager.remove(task.name)
63
64        self.dep_manager.close()
65
66    def _execute(self, dryrun, cleandep, cleanall, cleanforget,
67                 pos_args=None):
68        """Clean tasks
69        @param task_list (list - L{Task}): list of all tasks from dodo file
70        @ivar dryrun (bool): if True clean tasks are not executed
71                            (just print out what would be executed)
72        @param cleandep (bool): execute clean from task_dep
73        @param cleanall (bool): clean all tasks
74        @param cleanforget (bool): forget cleaned tasks
75        @var default_tasks (list - string): list of default tasks
76        @var selected_tasks (list - string): list of tasks selected
77                                             from cmd-line
78        """
79        tasks = TaskControl(self.task_list).tasks
80        # behavior of cleandep is different if selected_tasks comes from
81        # command line or DOIT_CONFIG.default_tasks
82        selected_tasks = pos_args
83        check_tasks_exist(tasks, selected_tasks)
84
85        # get base list of tasks to be cleaned
86        if selected_tasks and not cleanall: # from command line
87            clean_list = selected_tasks
88        else:
89            # if not cleaning specific task enable clean_dep automatically
90            cleandep = True
91            if self.sel_tasks is not None:
92                clean_list = self.sel_tasks # default tasks from config
93            else:
94                clean_list = [t.name for t in self.task_list]
95            # note: reversing is not required, but helps reversing
96            # execution order even if there are no restrictions about order.
97            clean_list.reverse()
98
99        tree = CleanDepTree()
100        # include dependencies in list
101        if cleandep:
102            for name in clean_list:
103                tree.build_nodes_with_deps(tasks, name)
104        # include only subtasks in list
105        else:
106            tree.build_nodes(tasks, clean_list)
107
108        to_clean = [tasks[x] for x in tree.flat()]
109        self.clean_tasks(to_clean, dryrun, cleanforget)
110
111
112class CleanDepTree:
113    """Create node structure where each node is a task and its children
114    are tasks that has the node as a task_dep/setup_task.
115    This creates an upside-down tree where leaf nodes should be
116    the first ones to be "cleaned".
117    """
118    def __init__(self):
119        self.nodes = OrderedDict()
120        self._processed = set() # task names that were already built
121
122    def build_nodes_with_deps(self, tasks, task_name):
123        """build node including task_dep's"""
124        if task_name in self._processed:
125            return
126        else:
127            self._processed.add(task_name)
128
129        # add node itself if not in list of nodes
130        self.nodes.setdefault(task_name, [])
131        task = tasks[task_name]
132        # reversing not required
133        for dep_name in reversed(task.setup_tasks + task.task_dep):
134            rev_dep = self.nodes.setdefault(dep_name, [])
135            rev_dep.append(task_name)
136            self.build_nodes_with_deps(tasks, dep_name)
137
138    def build_nodes(self, tasks, clean_list):
139        """build nodes with sub-tasks but no other task_dep"""
140        for name in clean_list:
141            # add node itself if not in list of nodes
142            self.nodes.setdefault(name, [])
143            task = tasks[name]
144            # reversing not required
145            for dep_name in reversed(task.task_dep):
146                if tasks[dep_name].subtask_of == name:
147                    rev_dep = self.nodes.setdefault(dep_name, [])
148                    rev_dep.append(name)
149
150    def flat(self):
151        """return list of tasks in the order they should be `clean` """
152        to_clean = []
153        while self.nodes:
154            head, children = self.nodes.popitem(0)
155            to_clean.extend([x for x in self._get_leafs(head, children)])
156        return to_clean
157
158    def _get_leafs(self, name, children):
159        for child_name in children:
160            if child_name in self.nodes:
161                grand = self.nodes.pop(child_name)
162                yield from self._get_leafs(child_name, grand)
163        yield name
164