File: bug.py

package info (click to toggle)
python-bugzilla 3.0.2-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, bullseye, sid
  • size: 1,072 kB
  • sloc: python: 5,760; makefile: 9
file content (511 lines) | stat: -rw-r--r-- 17,254 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
# Copyright (C) 2007, 2008, 2009, 2010 Red Hat Inc.
# Author: Will Woods <wwoods@redhat.com>
#
# This work is licensed under the GNU GPLv2 or later.
# See the COPYING file in the top-level directory.

import copy
from logging import getLogger


log = getLogger(__name__)


class Bug(object):
    """
    A container object for a bug report. Requires a Bugzilla instance -
    every Bug is on a Bugzilla, obviously.
    Optional keyword args:
        dict=DICT   - populate attributes with the result of a getBug() call
        bug_id=ID   - if dict does not contain bug_id, this is required before
                      you can read any attributes or make modifications to this
                      bug.
    """
    def __init__(self, bugzilla, bug_id=None, dict=None, autorefresh=False):
        # pylint: disable=redefined-builtin
        # API had pre-existing issue that we can't change ('dict' usage)

        self.bugzilla = bugzilla
        self._rawdata = {}
        self.autorefresh = autorefresh

        # pylint: disable=protected-access
        self._aliases = self.bugzilla._get_bug_aliases()
        # pylint: enable=protected-access

        if not dict:
            dict = {}
        if bug_id:
            dict["id"] = bug_id

        self._update_dict(dict)
        self.weburl = bugzilla.url.replace('xmlrpc.cgi',
                                           'show_bug.cgi?id=%i' % self.bug_id)

    def __str__(self):
        """
        Return a simple string representation of this bug
        """
        return self.__unicode__()

    def __unicode__(self):
        """
        Return a simple unicode string representation of this bug
        """
        return "#%-6s %-10s - %s - %s" % (self.bug_id, self.bug_status,
                                          self.assigned_to, self.summary)

    def __repr__(self):
        url = ""
        if self.bugzilla:
            url = self.bugzilla.url
        return '<Bug #%i on %s at %#x>' % (self.bug_id, url, id(self))

    def __getattr__(self, name):
        refreshed = False
        while True:
            if refreshed and name in self.__dict__:
                # If name was in __dict__ to begin with, __getattr__ would
                # have never been called.
                return self.__dict__[name]

            for newname, oldname in self._aliases:
                if name == oldname and newname in self.__dict__:
                    return self.__dict__[newname]

            # Doing dir(bugobj) does getattr __members__/__methods__,
            # don't refresh for those
            if name.startswith("__") and name.endswith("__"):
                break

            if refreshed or not self.autorefresh:
                break

            log.info("Bug %i missing attribute '%s' - doing implicit "
                "refresh(). This will be slow, if you want to avoid "
                "this, properly use query/getbug include_fields, and "
                "set bugzilla.bug_autorefresh = False to force failure.",
                self.bug_id, name)

            # We pass the attribute name to getbug, since for something like
            # 'attachments' which downloads lots of data we really want the
            # user to opt in.
            self.refresh(extra_fields=[name])
            refreshed = True

        msg = ("Bug object has no attribute '%s'." % name)
        if not self.autorefresh:
            msg += ("\nIf '%s' is a bugzilla attribute, it may not have "
                    "been cached when the bug was fetched. You may want "
                    "to adjust your include_fields for getbug/query." % name)
        raise AttributeError(msg)

    def get_raw_data(self):
        """
        Return the raw API dictionary data that has been used to
        populate this bug
        """
        return copy.deepcopy(self._rawdata)

    def refresh(self, include_fields=None, exclude_fields=None,
        extra_fields=None):
        """
        Refresh the bug with the latest data from bugzilla
        """
        # pylint: disable=protected-access
        extra_fields = list(self._rawdata.keys()) + (extra_fields or [])
        r = self.bugzilla._getbug(self.bug_id,
            include_fields=include_fields, exclude_fields=exclude_fields,
            extra_fields=extra_fields)
        # pylint: enable=protected-access
        self._update_dict(r)
    reload = refresh

    def _translate_dict(self, newdict):
        if self.bugzilla:
            self.bugzilla.post_translation({}, newdict)

        for newname, oldname in self._aliases:
            if oldname not in newdict:
                continue

            if newname not in newdict:
                newdict[newname] = newdict[oldname]
            elif newdict[newname] != newdict[oldname]:
                log.debug("Update dict contained differing alias values "
                          "d[%s]=%s and d[%s]=%s , dropping the value "
                          "d[%s]", newname, newdict[newname], oldname,
                        newdict[oldname], oldname)
            del(newdict[oldname])


    def _update_dict(self, newdict):
        """
        Update internal dictionary, in a way that ensures no duplicate
        entries are stored WRT field aliases
        """
        self._translate_dict(newdict)
        self._rawdata.update(newdict)
        self.__dict__.update(newdict)

        if 'id' not in self.__dict__ and 'bug_id' not in self.__dict__:
            raise TypeError("Bug object needs a bug_id")


    ##################
    # pickle helpers #
    ##################

    def __getstate__(self):
        ret = self._rawdata.copy()
        ret["_aliases"] = self._aliases
        return ret

    def __setstate__(self, vals):
        self._rawdata = {}
        self.bugzilla = None
        self._aliases = vals.get("_aliases", [])
        self.autorefresh = False
        self._update_dict(vals)


    #####################
    # Modify bug status #
    #####################

    def setstatus(self, status, comment=None, private=False):
        """
        Update the status for this bug report.
        Commonly-used values are ASSIGNED, MODIFIED, and NEEDINFO.

        To change bugs to CLOSED, use .close() instead.
        """
        # Note: fedora bodhi uses this function
        vals = self.bugzilla.build_update(status=status,
                                          comment=comment,
                                          comment_private=private)
        log.debug("setstatus: update=%s", vals)

        return self.bugzilla.update_bugs(self.bug_id, vals)

    def close(self, resolution, dupeid=None, fixedin=None,
              comment=None, isprivate=False):
        """
        Close this bug.
        Valid values for resolution are in bz.querydefaults['resolution_list']
        For bugzilla.redhat.com that's:
        ['NOTABUG', 'WONTFIX', 'DEFERRED', 'WORKSFORME', 'CURRENTRELEASE',
         'RAWHIDE', 'ERRATA', 'DUPLICATE', 'UPSTREAM', 'NEXTRELEASE',
         'CANTFIX', 'INSUFFICIENT_DATA']
        If using DUPLICATE, you need to set dupeid to the ID of the other bug.
        If using WORKSFORME/CURRENTRELEASE/RAWHIDE/ERRATA/UPSTREAM/NEXTRELEASE
          you can (and should) set 'new_fixed_in' to a string representing the
          version that fixes the bug.
        You can optionally add a comment while closing the bug. Set 'isprivate'
          to True if you want that comment to be private.
        """
        # Note: fedora bodhi uses this function
        vals = self.bugzilla.build_update(comment=comment,
                                          comment_private=isprivate,
                                          resolution=resolution,
                                          dupe_of=dupeid,
                                          fixed_in=fixedin,
                                          status=str("CLOSED"))
        log.debug("close: update=%s", vals)

        return self.bugzilla.update_bugs(self.bug_id, vals)


    #####################
    # Modify bug emails #
    #####################

    def setassignee(self, assigned_to=None,
                    qa_contact=None, comment=None):
        """
        Set any of the assigned_to or qa_contact fields to a new
        bugzilla account, with an optional comment, e.g.
        setassignee(assigned_to='wwoods@redhat.com')
        setassignee(qa_contact='wwoods@redhat.com', comment='wwoods QA ftw')

        You must set at least one of the two assignee fields, or this method
        will throw a ValueError.

        Returns [bug_id, mailresults].
        """
        if not (assigned_to or qa_contact):
            raise ValueError("You must set one of assigned_to "
                             " or qa_contact")

        vals = self.bugzilla.build_update(assigned_to=assigned_to,
                                          qa_contact=qa_contact,
                                          comment=comment)
        log.debug("setassignee: update=%s", vals)

        return self.bugzilla.update_bugs(self.bug_id, vals)

    def addcc(self, cclist, comment=None):
        """
        Adds the given email addresses to the CC list for this bug.
        cclist: list of email addresses (strings)
        comment: optional comment to add to the bug
        """
        vals = self.bugzilla.build_update(comment=comment,
                                          cc_add=cclist)
        log.debug("addcc: update=%s", vals)

        return self.bugzilla.update_bugs(self.bug_id, vals)

    def deletecc(self, cclist, comment=None):
        """
        Removes the given email addresses from the CC list for this bug.
        """
        vals = self.bugzilla.build_update(comment=comment,
                                          cc_remove=cclist)
        log.debug("deletecc: update=%s", vals)

        return self.bugzilla.update_bugs(self.bug_id, vals)


    ####################
    # comment handling #
    ####################

    def addcomment(self, comment, private=False):
        """
        Add the given comment to this bug. Set private to True to mark this
        comment as private.
        """
        # Note: fedora bodhi uses this function
        vals = self.bugzilla.build_update(comment=comment,
                                          comment_private=private)
        log.debug("addcomment: update=%s", vals)

        return self.bugzilla.update_bugs(self.bug_id, vals)

    def getcomments(self):
        """
        Returns an array of comment dictionaries for this bug
        """
        comment_list = self.bugzilla.get_comments([self.bug_id])
        return comment_list['bugs'][str(self.bug_id)]['comments']


    #####################
    # Get/Set bug flags #
    #####################

    def get_flag_type(self, name):
        """
        Return flag_type information for a specific flag

        Older RHBugzilla returned a lot more info here, but it was
        non-upstream and is now gone.
        """
        for t in self.flags:
            if t['name'] == name:
                return t
        return None

    def get_flags(self, name):
        """
        Return flag value information for a specific flag
        """
        ft = self.get_flag_type(name)
        if not ft:
            return None

        return [ft]

    def get_flag_status(self, name):
        """
        Return a flag 'status' field

        This method works only for simple flags that have only a 'status' field
        with no "requestee" info, and no multiple values. For more complex
        flags, use get_flags() to get extended flag value information.
        """
        f = self.get_flags(name)
        if not f:
            return None

        # This method works only for simple flags that have only one
        # value set.
        assert len(f) <= 1

        return f[0]['status']

    def updateflags(self, flags):
        """
        Thin wrapper around build_update(flags=X). This only handles simple
        status changes, anything like needinfo requestee needs to call
        build_update + update_bugs directly

        :param flags: Dictionary of the form {"flagname": "status"}, example
            {"needinfo": "?", "devel_ack": "+"}
        """
        flaglist = []
        for key, value in flags.items():
            flaglist.append({"name": key, "status": value})
        return self.bugzilla.update_bugs([self.bug_id],
            self.bugzilla.build_update(flags=flaglist))


    ########################
    # Experimental methods #
    ########################

    def get_attachments(self, include_fields=None, exclude_fields=None):
        """
        Helper call to Bugzilla.get_attachments. If you want to fetch
        specific attachment IDs, use that function instead
        """
        if "attachments" in self.__dict__:
            return self.attachments

        data = self.bugzilla.get_attachments([self.bug_id], None,
                include_fields, exclude_fields)
        return data["bugs"][str(self.bug_id)]

    def get_attachment_ids(self):
        """
        Helper function to return only the attachment IDs for this bug
        """
        return [a["id"] for a in self.get_attachments(exclude_fields=["data"])]

    def get_history_raw(self):
        """
        Experimental. Get the history of changes for this bug.
        """
        return self.bugzilla.bugs_history_raw([self.bug_id])


class User(object):
    """
    Container object for a bugzilla User.

    :arg bugzilla: Bugzilla instance that this User belongs to.
    Rest of the params come straight from User.get()
    """
    def __init__(self, bugzilla, **kwargs):
        self.bugzilla = bugzilla
        self.__userid = kwargs.get('id')
        self.__name = kwargs.get('name')

        self.__email = kwargs.get('email', self.__name)
        self.__can_login = kwargs.get('can_login', False)

        self.real_name = kwargs.get('real_name', None)
        self.password = None

        self.groups = kwargs.get('groups', {})
        self.groupnames = []
        for g in self.groups:
            if "name" in g:
                self.groupnames.append(g["name"])
        self.groupnames.sort()


    ########################
    # Read-only attributes #
    ########################

    # We make these properties so that the user cannot set them.  They are
    # unaffected by the update() method so it would be misleading to let them
    # be changed.
    @property
    def userid(self):
        return self.__userid

    @property
    def email(self):
        return self.__email

    @property
    def can_login(self):
        return self.__can_login

    # name is a key in some methods.  Mark it dirty when we change it #
    @property
    def name(self):
        return self.__name

    def refresh(self):
        """
        Update User object with latest info from bugzilla
        """
        newuser = self.bugzilla.getuser(self.email)
        self.__dict__.update(newuser.__dict__)

    def updateperms(self, action, groups):
        """
        A method to update the permissions (group membership) of a bugzilla
        user.

        :arg action: add, remove, or set
        :arg groups: list of groups to be added to (i.e. ['fedora_contrib'])
        """
        self.bugzilla.updateperms(self.name, action, groups)


class Group(object):
    """
    Container object for a bugzilla Group.

    :arg bugzilla: Bugzilla instance that this Group belongs to.
    Rest of the params come straight from Group.get()
    """
    def __init__(self, bugzilla, **kwargs):
        self.bugzilla = bugzilla
        self.__groupid = kwargs.get('id')

        self.name = kwargs.get('name')
        self.description = kwargs.get('description', self.name)
        self.is_active = kwargs.get('is_active', False)
        self.icon_url = kwargs.get('icon_url', None)
        self.is_active_bug_group = kwargs.get('is_active_bug_group', None)

        self.membership = kwargs.get('membership', [])
        self.__member_emails = set()
        self._refresh_member_emails_list()

    ########################
    # Read-only attributes #
    ########################

    # We make these properties so that the user cannot set them.  They are
    # unaffected by the update() method so it would be misleading to let them
    # be changed.
    @property
    def groupid(self):
        return self.__groupid

    @property
    def member_emails(self):
        return sorted(self.__member_emails)

    def _refresh_member_emails_list(self):
        """
        Refresh the list of emails of the members of the group.
        """
        if self.membership:
            for m in self.membership:
                if "email" in m:
                    self.__member_emails.add(m["email"])

    def refresh(self, membership=False):
        """
        Update Group object with latest info from bugzilla
        """
        newgroup = self.bugzilla.getgroup(
            self.name, membership=membership)
        self.__dict__.update(newgroup.__dict__)
        self._refresh_member_emails_list()

    def members(self):
        """
        Retrieve the members of this Group from bugzilla
        """
        if not self.membership:
            self.refresh(membership=True)
        return self.membership