~jocave/checkbox/hybrid-amd-gpu-mods

« back to all changes in this revision

Viewing changes to checkbox-ng/checkbox_ng/misc.py

  • Committer: Tarmac
  • Author(s): Brendan Donegan
  • Date: 2013-06-03 11:12:58 UTC
  • mfrom: (2154.2.1 bug1185759)
  • Revision ID: tarmac-20130603111258-1b3m5ydvkf1accts
"[r=zkrynicki][bug=1185759][author=brendan-donegan] automatic merge by tarmac"

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# This file is part of Checkbox.
2
 
#
3
 
# Copyright 2013-2014 Canonical Ltd.
4
 
# Written by:
5
 
#   Sylvain Pineau <sylvain.pineau@canonical.com>
6
 
#
7
 
# Checkbox is free software: you can redistribute it and/or modify
8
 
# it under the terms of the GNU General Public License version 3,
9
 
# as published by the Free Software Foundation.
10
 
#
11
 
# Checkbox is distributed in the hope that it will be useful,
12
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 
# GNU General Public License for more details.
15
 
#
16
 
# You should have received a copy of the GNU General Public License
17
 
# along with Checkbox.  If not, see <http://www.gnu.org/licenses/>.
18
 
 
19
 
"""
20
 
:mod:`checkbox_ng.misc` -- Other stuff
21
 
======================================
22
 
 
23
 
.. warning::
24
 
 
25
 
    THIS MODULE DOES NOT HAVE STABLE PUBLIC API
26
 
"""
27
 
 
28
 
from logging import getLogger
29
 
 
30
 
 
31
 
logger = getLogger("checkbox.ng.commands.cli")
32
 
 
33
 
 
34
 
class JobTreeNode:
35
 
 
36
 
    r"""
37
 
    JobTreeNode class is used to store a tree structure.
38
 
 
39
 
    A tree consists of a collection of JobTreeNode instances connected in a
40
 
    hierarchical way where nodes are used as categories, jobs belonging to a
41
 
    category are listed in the node leaves.
42
 
 
43
 
    Example::
44
 
               / Job A
45
 
         Root-|
46
 
              |                 / Job B
47
 
               \--- Category X |
48
 
                                \ Job C
49
 
    """
50
 
 
51
 
    def __init__(self, name=None):
52
 
        """ Initialize the job tree node with a given name. """
53
 
        self._name = name if name else 'Root'
54
 
        self._parent = None
55
 
        self._categories = []
56
 
        self._jobs = []
57
 
 
58
 
    @property
59
 
    def name(self):
60
 
        """ name of this node. """
61
 
        return self._name
62
 
 
63
 
    @property
64
 
    def parent(self):
65
 
        """ parent node for this node. """
66
 
        return self._parent
67
 
 
68
 
    @property
69
 
    def categories(self):
70
 
        """ list of sub categories. """
71
 
        return self._categories
72
 
 
73
 
    @property
74
 
    def jobs(self):
75
 
        """ job(s) belonging to this node/category. """
76
 
        return self._jobs
77
 
 
78
 
    @property
79
 
    def depth(self):
80
 
        """ level of depth for this node. """
81
 
        return (self._parent.depth + 1) if self._parent else 0
82
 
 
83
 
    def __str__(self):
84
 
        """ same as self.name. """
85
 
        return self.name
86
 
 
87
 
    def __repr__(self):
88
 
        """ Get a representation of this node for debugging. """
89
 
        return "<JobTreeNode name:{!r}>".format(self.name)
90
 
 
91
 
    def add_category(self, category):
92
 
        """
93
 
        Add a new category to this node.
94
 
 
95
 
        :param category:
96
 
            The node instance to be added as a category.
97
 
        """
98
 
        self._categories.append(category)
99
 
        # Always keep this list sorted to easily find a given child by index
100
 
        self._categories.sort(key=lambda item: item.name)
101
 
        category._parent = self
102
 
 
103
 
    def add_job(self, job):
104
 
        """
105
 
        Add a new job to this node.
106
 
 
107
 
        :param job:
108
 
            The job instance to be added to this node.
109
 
        """
110
 
        self._jobs.append(job)
111
 
        # Always keep this list sorted to easily find a given leaf by index
112
 
        # Note bisect.insort(a, x) cannot be used here as JobDefinition are
113
 
        # not sortable
114
 
        self._jobs.sort(key=lambda item: item.id)
115
 
 
116
 
    def get_ancestors(self):
117
 
        """ Get the list of ancestors from here to the root of the tree.  """
118
 
        ancestors = []
119
 
        node = self
120
 
        while node.parent is not None:
121
 
            ancestors.append(node.parent)
122
 
            node = node.parent
123
 
        return ancestors
124
 
 
125
 
    def get_descendants(self):
126
 
        """ Return a list of all descendant category nodes.  """
127
 
        descendants = []
128
 
        for category in self.categories:
129
 
            descendants.append(category)
130
 
            descendants.extend(category.get_descendants())
131
 
        return descendants
132
 
 
133
 
    @classmethod
134
 
    def create_tree(cls, session_state, job_list):
135
 
        """
136
 
        Build a rooted JobTreeNode from a job list.
137
 
 
138
 
        :argument session_state:
139
 
            A session state object
140
 
        :argument job_list:
141
 
            List of jobs to consider for building the tree.
142
 
        """
143
 
        builder = TreeBuilder(session_state, cls)
144
 
        for job in job_list:
145
 
            builder.auto_add_job(job)
146
 
        return builder.root_node
147
 
 
148
 
    @classmethod
149
 
    def create_simple_tree(cls, sa, job_list):
150
 
        """
151
 
        Build a rooted JobTreeNode from a job list.
152
 
 
153
 
        :argument sa:
154
 
            A session assistant object
155
 
        :argument job_list:
156
 
            List of jobs to consider for building the tree.
157
 
        """
158
 
        root_node = cls()
159
 
        for job in job_list:
160
 
            cat_name = sa.get_category(job.category_id).tr_name()
161
 
            matches = [n for n in root_node.categories if n.name == cat_name]
162
 
            if not matches:
163
 
                node = cls(cat_name)
164
 
                root_node.add_category(node)
165
 
            else:
166
 
                node = matches[0]
167
 
            node.add_job(job)
168
 
        return root_node
169
 
 
170
 
 
171
 
class TreeBuilder:
172
 
 
173
 
    """
174
 
    Builder for :class:`JobTreeNode`.
175
 
 
176
 
 
177
 
    Helper class that assists in building a tree of :class:`JobTreeNode`
178
 
    objects out of job definitions and their associations, as expressed by
179
 
    :attr:`JobState.via_job` associated with each job.
180
 
 
181
 
    The builder is a single-use object and should be re-created for each new
182
 
    construct. Internally it stores the job_state_map of the
183
 
    :class:`SessionState` it was created with as well as additional helper
184
 
    state.
185
 
    """
186
 
 
187
 
    def __init__(self, session_state: "SessionState", node_cls):
188
 
        self._job_state_map = session_state.job_state_map
189
 
        self._node_cls = node_cls
190
 
        self._root_node = node_cls()
191
 
        self._category_node_map = {}  # id -> node
192
 
 
193
 
    @property
194
 
    def root_node(self):
195
 
        return self._root_node
196
 
 
197
 
    def auto_add_job(self, job):
198
 
        """
199
 
        Add a job to the tree, automatically creating category nodes as needed.
200
 
 
201
 
        :param job:
202
 
            The job definition to add.
203
 
        """
204
 
        if job.plugin == 'local':
205
 
            # For local jobs, just create the category node but don't add the
206
 
            # local job itself there.
207
 
            self.get_or_create_category_node(job)
208
 
        else:
209
 
            # For all other jobs, look at the parent job (if any) and create
210
 
            # the category node out of that node. This never fails as "None" is
211
 
            # the root_node object.
212
 
            state = self._job_state_map[job.id]
213
 
            node = self.get_or_create_category_node(state.via_job)
214
 
            # Then add that job to the category node
215
 
            node.add_job(job)
216
 
 
217
 
    def get_or_create_category_node(self, category_job):
218
 
        """
219
 
        Get a category node for a given job.
220
 
 
221
 
        Get or create a :class:`JobTreeNode` that corresponds to the
222
 
        category defined (somehow) by the job ``category_job``.
223
 
 
224
 
        :param category_job:
225
 
            The job that describes the category. This is either a
226
 
            plugin="local" job or a plugin="resource" job. This can also be
227
 
            None, which is a shorthand to say "root node".
228
 
        :returns:
229
 
            The ``root_node`` if ``category_job`` is None. A freshly
230
 
            created node, created with :func:`create_category_node()` if
231
 
            the category_job was never seen before (as recorded by the
232
 
            category_node_map).
233
 
        """
234
 
        logger.debug("get_or_create_category_node(%r)", category_job)
235
 
        if category_job is None:
236
 
            return self._root_node
237
 
        if category_job.id not in self._category_node_map:
238
 
            category_node = self.create_category_node(category_job)
239
 
            # The category is added to its parent, that's either the root
240
 
            # (if we're standalone) or the non-root category this one
241
 
            # belongs to.
242
 
            category_state = self._job_state_map[category_job.id]
243
 
            if category_state.via_job is not None:
244
 
                parent_category_node = self.get_or_create_category_node(
245
 
                    category_state.via_job)
246
 
            else:
247
 
                parent_category_node = self._root_node
248
 
            parent_category_node.add_category(category_node)
249
 
        else:
250
 
            category_node = self._category_node_map[category_job.id]
251
 
        return category_node
252
 
 
253
 
    def create_category_node(self, category_job):
254
 
        """
255
 
        Create a category node for a given job.
256
 
 
257
 
        Create a :class:`JobTreeNode` that corresponds to the category defined
258
 
        (somehow) by the job ``category_job``.
259
 
 
260
 
        :param category_job:
261
 
            The job that describes the node to create.
262
 
        :returns:
263
 
            A fresh node with appropriate data.
264
 
        """
265
 
        logger.debug("create_category_node(%r)", category_job)
266
 
        if category_job.summary == category_job.partial_id:
267
 
            category_node = self._node_cls(category_job.description)
268
 
        else:
269
 
            category_node = self._node_cls(category_job.summary)
270
 
        self._category_node_map[category_job.id] = category_node
271
 
        return category_node
272
 
 
273
 
 
274
 
class SelectableJobTreeNode(JobTreeNode):
275
 
    """
276
 
    Implementation of a node in a tree that can be selected/deselected
277
 
    """
278
 
    def __init__(self, job=None):
279
 
        super().__init__(job)
280
 
        self.selected = True
281
 
        self.job_selection = {}
282
 
        self.expanded = True
283
 
        self.current_index = 0
284
 
        self._resource_jobs = []
285
 
 
286
 
    def get_node_by_index(self, index, tree=None):
287
 
        """
288
 
        Return the node found at the position given by index considering the
289
 
        tree from a top-down list view.
290
 
        """
291
 
        if tree is None:
292
 
            tree = self
293
 
        if self.expanded:
294
 
            for category in self.categories:
295
 
                if index == tree.current_index:
296
 
                    tree.current_index = 0
297
 
                    return (category, None)
298
 
                else:
299
 
                    tree.current_index += 1
300
 
                result = category.get_node_by_index(index, tree)
301
 
                if result != (None, None):
302
 
                    return result
303
 
            for job in self.jobs:
304
 
                if index == tree.current_index:
305
 
                    tree.current_index = 0
306
 
                    return (job, self)
307
 
                else:
308
 
                    tree.current_index += 1
309
 
        return (None, None)
310
 
 
311
 
    def render(self, cols=80, as_summary=True):
312
 
        """
313
 
        Return the tree as a simple list of categories and jobs suitable for
314
 
        display. Jobs are properly indented to respect the tree hierarchy
315
 
        and selection marks are added automatically at the beginning of each
316
 
        element.
317
 
 
318
 
        The node titles should not exceed the width of a the terminal and
319
 
        thus are cut to fit inside.
320
 
 
321
 
        :param cols:
322
 
            The number of columns to render.
323
 
        :param as_summary:
324
 
            Whether we display the job summaries or their partial IDs.
325
 
        """
326
 
        self._flat_list = []
327
 
        if self.expanded:
328
 
            for category in self.categories:
329
 
                prefix = '[ ]'
330
 
                if category.selected:
331
 
                    prefix = '[X]'
332
 
                line = ''
333
 
                title = category.name
334
 
                if category.jobs or category.categories:
335
 
                    if category.expanded:
336
 
                        line = prefix + self.depth * '   ' + ' - ' + title
337
 
                    else:
338
 
                        line = prefix + self.depth * '   ' + ' + ' + title
339
 
                else:
340
 
                    line = prefix + self.depth * '   ' + '   ' + title
341
 
                if len(line) > cols:
342
 
                    col_max = cols - 4  # includes len('...') + a space
343
 
                    line = line[:col_max] + '...'
344
 
                self._flat_list.append(line)
345
 
                self._flat_list.extend(category.render(cols, as_summary))
346
 
            for job in self.jobs:
347
 
                prefix = '[ ]'
348
 
                if self.job_selection[job]:
349
 
                    prefix = '[X]'
350
 
                if as_summary:
351
 
                    title = job.tr_summary()
352
 
                else:
353
 
                    title = job.partial_id
354
 
                line = prefix + self.depth * '   ' + '   ' + title
355
 
                if len(line) > cols:
356
 
                    col_max = cols - 4  # includes len('...') + a space
357
 
                    line = line[:col_max] + '...'
358
 
                self._flat_list.append(line)
359
 
        return self._flat_list
360
 
 
361
 
    def add_job(self, job):
362
 
        if job.plugin == 'resource':
363
 
            # I don't want the user to see resources but I need to keep
364
 
            # track of them to put them in the final selection. I also
365
 
            # don't want to add them to the tree.
366
 
            self._resource_jobs.append(job)
367
 
            return
368
 
        super().add_job(job)
369
 
        self.job_selection[job] = True
370
 
 
371
 
    @property
372
 
    def selection(self):
373
 
        """
374
 
        Return all the jobs currently selected
375
 
        """
376
 
        self._selection_list = []
377
 
        for category in self.categories:
378
 
            self._selection_list.extend(category.selection)
379
 
        for job in self.job_selection:
380
 
            if self.job_selection[job]:
381
 
                self._selection_list.append(job)
382
 
        return self._selection_list
383
 
 
384
 
    @property
385
 
    def resource_jobs(self):
386
 
        """Return all the resource jobs."""
387
 
        return self._resource_jobs
388
 
 
389
 
    def set_ancestors_state(self, new_state):
390
 
        """
391
 
        Set the selection state of all ancestors consistently
392
 
        """
393
 
        # If child is set, then all ancestors must be set
394
 
        if new_state:
395
 
            parent = self.parent
396
 
            while parent:
397
 
                parent.selected = new_state
398
 
                parent = parent.parent
399
 
        # If child is not set, then all ancestors mustn't be set
400
 
        # unless another child of the ancestor is set
401
 
        else:
402
 
            parent = self.parent
403
 
            while parent:
404
 
                if any((category.selected
405
 
                        for category in parent.categories)):
406
 
                    break
407
 
                if any((parent.job_selection[job]
408
 
                        for job in parent.job_selection)):
409
 
                    break
410
 
                parent.selected = new_state
411
 
                parent = parent.parent
412
 
 
413
 
    def update_selected_state(self):
414
 
        """
415
 
        Update the category state according to its job selection
416
 
        """
417
 
        if any((self.job_selection[job] for job in self.job_selection)):
418
 
            self.selected = True
419
 
        else:
420
 
            self.selected = False
421
 
 
422
 
    def set_descendants_state(self, new_state):
423
 
        """
424
 
        Set the selection state of all descendants recursively
425
 
        """
426
 
        self.selected = new_state
427
 
        for job in self.job_selection:
428
 
            self.job_selection[job] = new_state
429
 
        for category in self.categories:
430
 
            category.set_descendants_state(new_state)