ciabot/INSTALL | 4 ciabot/README | 4 ciabot/bugzilla/__init__.py | 133 --- ciabot/bugzilla/base.py | 1758 ------------------------------------------ ciabot/bugzilla/bug.py | 517 ------------ ciabot/bugzilla/bugzilla3.py | 34 ciabot/bugzilla/bugzilla4.py | 47 - ciabot/bugzilla/rhbugzilla.py | 368 -------- lionss/tpl/footer.html | 2 9 files changed, 5 insertions(+), 2862 deletions(-)
New commits: commit 1b8d02e74b2b391c7a6adea7150134f815f1b147 Author: Guilhem Moulin <guil...@libreoffice.org> AuthorDate: Sat Feb 22 05:14:28 2020 +0100 Commit: Guilhem Moulin <guil...@libreoffice.org> CommitDate: Sat Feb 22 05:14:58 2020 +0100 Upgrade irc:// URIs to ircs:// diff --git a/ciabot/README b/ciabot/README index 6124f53..4ea4e1b 100644 --- a/ciabot/README +++ b/ciabot/README @@ -38,8 +38,8 @@ How to test the Bugzilla integration: - Edit file projmap.json to change the IRC channel -e.g. "to": "irc://irc.freenode.net/libreoffice-dev" -> - "to": "irc://irc.freenode.net/libreoffice-dev-test" +e.g. "to": "ircs://irc.freenode.net/libreoffice-dev" -> + "to": "ircs://irc.freenode.net/libreoffice-dev-test" - Edit config.cfg to change the url for the Bugzilla install diff --git a/lionss/tpl/footer.html b/lionss/tpl/footer.html index 134ff27..4dbb275 100644 --- a/lionss/tpl/footer.html +++ b/lionss/tpl/footer.html @@ -1,6 +1,6 @@ <div id="footer"> If you need help or want to discuss, do not hesitate to join - <a href="irc://chat.freenode.net/libreoffice-dev">#libreoffice-dev on freenode IRC</a> + <a href="ircs://chat.freenode.net/libreoffice-dev">#libreoffice-dev on freenode IRC</a> </div> </body> commit 8f5898d563bf6f638f6ad27f0951d1d598e10bec Author: Guilhem Moulin <guil...@libreoffice.org> AuthorDate: Sat Feb 22 03:28:40 2020 +0100 Commit: Guilhem Moulin <guil...@libreoffice.org> CommitDate: Sat Feb 22 05:14:50 2020 +0100 ciabot: Remove python-bugzilla. Let's just use the version from the distro instead. Debian Stretch has 1.2.2, Buster 2.2.0-1. diff --git a/ciabot/INSTALL b/ciabot/INSTALL index ddc2da3..3f36105 100644 --- a/ciabot/INSTALL +++ b/ciabot/INSTALL @@ -2,8 +2,8 @@ The following steps are needed to use this code: Install some packages: - Python - * git module (ubuntu: python-git) - * bugzilla module (?) (ubuntu: not in standard repos) + * git module (Debian: python-git) + * bugzilla module (Debian: python-bugzilla) Configure the software: - Copy config-example.cfg -> config.cfg and change the defaults diff --git a/ciabot/bugzilla/__init__.py b/ciabot/bugzilla/__init__.py deleted file mode 100644 index 382db52..0000000 --- a/ciabot/bugzilla/__init__.py +++ /dev/null @@ -1,133 +0,0 @@ -# python-bugzilla - a Python interface to bugzilla using xmlrpclib. -# -# Copyright (C) 2007, 2008 Red Hat Inc. -# Author: Will Woods <wwo...@redhat.com> -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the -# Free Software Foundation; either version 2 of the License, or (at your -# option) any later version. See http://www.gnu.org/copyleft/gpl.html for -# the full text of the license. - -__version__ = "1.1.0" -version = __version__ - -import sys -from logging import getLogger - -if hasattr(sys.version_info, "major") and sys.version_info.major >= 3: - # pylint: disable=F0401 - from xmlrpc.client import Fault, ServerProxy -else: - from xmlrpclib import Fault, ServerProxy - -log = getLogger("bugzilla") - - -from bugzilla.base import BugzillaBase as _BugzillaBase -from bugzilla.base import BugzillaError -from bugzilla.base import RequestsTransport as _RequestsTransport -from bugzilla.bugzilla3 import Bugzilla3, Bugzilla32, Bugzilla34, Bugzilla36 -from bugzilla.bugzilla4 import Bugzilla4, Bugzilla42, Bugzilla44 -from bugzilla.rhbugzilla import RHBugzilla, RHBugzilla3, RHBugzilla4 - - -# Back compat for deleted NovellBugzilla -class NovellBugzilla(Bugzilla34): - pass - - -def _getBugzillaClassForURL(url, sslverify): - url = Bugzilla3.fix_url(url) - log.debug("Detecting subclass for %s", url) - s = ServerProxy(url, _RequestsTransport(url, sslverify=sslverify)) - rhbz = False - bzversion = '' - c = None - - if "bugzilla.redhat.com" in url: - log.info("Using RHBugzilla for URL containing bugzilla.redhat.com") - return RHBugzilla - if "bugzilla.novell.com" in url: - log.info("Using NovellBugzilla for URL containing novell.com") - return NovellBugzilla - - # Check for a Red Hat extension - try: - log.debug("Checking for Red Hat Bugzilla extension") - extensions = s.Bugzilla.extensions() - if extensions.get('extensions', {}).get('RedHat', False): - rhbz = True - except Fault: - pass - log.debug("rhbz=%s", str(rhbz)) - - # Try to get the bugzilla version string - try: - log.debug("Checking return value of Bugzilla.version()") - r = s.Bugzilla.version() - bzversion = r['version'] - except Fault: - pass - log.debug("bzversion='%s'", str(bzversion)) - - # note preference order: RHBugzilla* wins if available - if rhbz: - c = RHBugzilla - elif bzversion.startswith("4."): - if bzversion.startswith("4.0"): - c = Bugzilla4 - elif bzversion.startswith("4.2"): - c = Bugzilla42 - else: - log.debug("No explicit match for %s, using latest bz4", bzversion) - c = Bugzilla44 - else: - if bzversion.startswith('3.6'): - c = Bugzilla36 - elif bzversion.startswith('3.4'): - c = Bugzilla34 - elif bzversion.startswith('3.2'): - c = Bugzilla32 - else: - log.debug("No explicit match for %s, fall through", bzversion) - c = Bugzilla3 - - return c - - -class Bugzilla(_BugzillaBase): - ''' - Magical Bugzilla class that figures out which Bugzilla implementation - to use and uses that. - ''' - def _init_class_from_url(self, url, sslverify): - if url is None: - raise TypeError("You must pass a valid bugzilla URL") - - c = _getBugzillaClassForURL(url, sslverify) - if not c: - raise ValueError("Couldn't determine Bugzilla version for %s" % - url) - - self.__class__ = c - log.info("Chose subclass %s v%s", c.__name__, c.version) - return True - - -# This is the list of possible Bugzilla instances an app can use, -# bin/bugzilla uses it for the --bztype field -classlist = [ - "Bugzilla3", "Bugzilla32", "Bugzilla34", "Bugzilla36", - "Bugzilla4", "Bugzilla42", "Bugzilla44", - "RHBugzilla3", "RHBugzilla4", "RHBugzilla", - "NovellBugzilla", -] - -# This is the public API. If you are explicitly instantiating any other -# class, using some function, or poking into internal files, don't complain -# if things break on you. -__all__ = classlist + [ - 'BugzillaError', - 'Bugzilla', -] diff --git a/ciabot/bugzilla/base.py b/ciabot/bugzilla/base.py deleted file mode 100644 index 81dffb4..0000000 --- a/ciabot/bugzilla/base.py +++ /dev/null @@ -1,1758 +0,0 @@ -# base.py - the base classes etc. for a Python interface to bugzilla -# -# Copyright (C) 2007, 2008, 2009, 2010 Red Hat Inc. -# Author: Will Woods <wwo...@redhat.com> -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the -# Free Software Foundation; either version 2 of the License, or (at your -# option) any later version. See http://www.gnu.org/copyleft/gpl.html for -# the full text of the license. - -import locale -import os -import sys - -from io import BytesIO - -if hasattr(sys.version_info, "major") and sys.version_info.major >= 3: - # pylint: disable=F0401,E0611 - from configparser import SafeConfigParser - from http.cookiejar import LoadError, LWPCookieJar, MozillaCookieJar - from urllib.parse import urlparse, parse_qsl - from xmlrpc.client import ( - Binary, Fault, ProtocolError, ServerProxy, Transport) -else: - from ConfigParser import SafeConfigParser - from cookielib import LoadError, LWPCookieJar, MozillaCookieJar - from urlparse import urlparse, parse_qsl - from xmlrpclib import ( - Binary, Fault, ProtocolError, ServerProxy, Transport) - -import requests - -from bugzilla import __version__, log -from bugzilla.bug import _Bug, _User - - -# Backwards compatibility -Bug = _Bug - -mimemagic = None - - -def _detect_filetype(fname): - # pylint: disable=E1103 - # E1103: Instance of 'bool' has no '%s' member - # pylint confuses mimemagic to be of type 'bool' - global mimemagic - - if mimemagic is None: - try: - # pylint: disable=F0401 - # F0401: Unable to import 'magic' (import-error) - import magic - mimemagic = magic.open(getattr(magic, "MAGIC_MIME_TYPE", 16)) - mimemagic.load() - except ImportError: - e = sys.exc_info()[1] - log.debug("Could not load python-magic: %s", e) - mimemagic = False - if mimemagic is False: - return None - - if not os.path.isabs(fname): - return None - - try: - return mimemagic.file(fname) - except Exception: - e = sys.exc_info()[1] - log.debug("Could not detect content_type: %s", e) - return None - - -def _build_cookiejar(cookiefile): - cj = MozillaCookieJar(cookiefile) - if cookiefile is None: - return cj - if not os.path.exists(cookiefile): - # Make sure a new file has correct permissions - open(cookiefile, 'a').close() - os.chmod(cookiefile, 0o600) - cj.save() - return cj - - # We always want to use Mozilla cookies, but we previously accepted - # LWP cookies. If we see the latter, convert it to former - try: - cj.load() - return cj - except LoadError: - pass - - try: - cj = LWPCookieJar(cookiefile) - cj.load() - except LoadError: - raise BugzillaError("cookiefile=%s not in LWP or Mozilla format" % - cookiefile) - - retcj = MozillaCookieJar(cookiefile) - for cookie in cj: - retcj.set_cookie(cookie) - retcj.save() - return retcj - - -class _BugzillaToken(object): - def __init__(self, uri, tokenfilename): - self.tokenfilename = tokenfilename - self.tokenfile = SafeConfigParser() - self.domain = urlparse(uri)[1] - - if self.tokenfilename: - self.tokenfile.read(self.tokenfilename) - - if self.domain not in self.tokenfile.sections(): - self.tokenfile.add_section(self.domain) - - @property - def value(self): - if self.tokenfile.has_option(self.domain, 'token'): - return self.tokenfile.get(self.domain, 'token') - else: - return None - - @value.setter - def value(self, value): - if self.value == value: - return - - if value is None: - self.tokenfile.remove_option(self.domain, 'token') - else: - self.tokenfile.set(self.domain, 'token', value) - - if self.tokenfilename: - with open(self.tokenfilename, 'w') as tokenfile: - log.debug("Saving to tokenfile") - self.tokenfile.write(tokenfile) - - def __repr__(self): - return '<Bugzilla Token :: %s>' % (self.value) - - -class _BugzillaServerProxy(ServerProxy): - def __init__(self, uri, tokenfile, *args, **kwargs): - # pylint: disable=super-init-not-called - # No idea why pylint complains here, must be a bug - ServerProxy.__init__(self, uri, *args, **kwargs) - self.token = _BugzillaToken(uri, tokenfile) - - def clear_token(self): - self.token.value = None - - def _ServerProxy__request(self, methodname, params): - if self.token.value is not None: - if len(params) == 0: - params = ({}, ) - - if 'Bugzilla_token' not in params[0]: - params[0]['Bugzilla_token'] = self.token.value - - # pylint: disable=maybe-no-member - ret = ServerProxy._ServerProxy__request(self, methodname, params) - # pylint: enable=maybe-no-member - - if isinstance(ret, dict) and 'token' in ret.keys(): - self.token.value = ret.get('token') - return ret - - -class RequestsTransport(Transport): - user_agent = 'Python/Bugzilla' - - def __init__(self, url, cookiejar=None, - sslverify=True, sslcafile=None, debug=0): - # pylint: disable=W0231 - # pylint does not handle multiple import of Transport well - if hasattr(Transport, "__init__"): - Transport.__init__(self, use_datetime=False) - - self.verbose = debug - self._cookiejar = cookiejar - - # transport constructor needs full url too, as xmlrpc does not pass - # scheme to request - self.scheme = urlparse(url)[0] - if self.scheme not in ["http", "https"]: - raise Exception("Invalid URL scheme: %s (%s)" % (self.scheme, url)) - - self.use_https = self.scheme == 'https' - - self.request_defaults = { - 'cert': sslcafile if self.use_https else None, - 'cookies': cookiejar, - 'verify': sslverify, - 'headers': { - 'Content-Type': 'text/xml', - 'User-Agent': self.user_agent, - } - } - - def parse_response(self, response): - """ Parse XMLRPC response """ - parser, unmarshaller = self.getparser() - parser.feed(response.text.encode('utf-8')) - parser.close() - return unmarshaller.close() - - def _request_helper(self, url, request_body): - """ - A helper method to assist in making a request and provide a parsed - response. - """ - response = None - try: - response = requests.post( - url, data=request_body, **self.request_defaults) - - # We expect utf-8 from the server - response.encoding = 'UTF-8' - - # update/set any cookies - if self._cookiejar is not None: - for cookie in response.cookies: - self._cookiejar.set_cookie(cookie) - - if self._cookiejar.filename is not None: - # Save is required only if we have a filename - self._cookiejar.save() - - response.raise_for_status() - return self.parse_response(response) - except requests.RequestException: - e = sys.exc_info()[1] - if not response: - raise - raise ProtocolError( - url, response.status_code, str(e), response.headers) - except Fault: - raise sys.exc_info()[1] - except Exception: - # pylint: disable=W0201 - e = BugzillaError(str(sys.exc_info()[1])) - e.__traceback__ = sys.exc_info()[2] - raise e - - def request(self, host, handler, request_body, verbose=0): - self.verbose = verbose - url = "%s://%s%s" % (self.scheme, host, handler) - - # xmlrpclib fails to escape \r - request_body = request_body.replace(b'\r', b'
') - - return self._request_helper(url, request_body) - - -class BugzillaError(Exception): - '''Error raised in the Bugzilla client code.''' - pass - - -class _FieldAlias(object): - """ - Track API attribute names that differ from what we expose in users. - - For example, originally 'short_desc' was the name of the property that - maps to 'summary' on modern bugzilla. We want pre-existing API users - to be able to continue to use Bug.short_desc, and - query({"short_desc": "foo"}). This class tracks that mapping. - - @oldname: The old attribute name - @newname: The modern attribute name - @is_api: If True, use this mapping for values sent to the xmlrpc API - (like the query example) - @is_bug: If True, use this mapping for Bug attribute names. - """ - def __init__(self, newname, oldname, is_api=True, is_bug=True): - self.newname = newname - self.oldname = oldname - self.is_api = is_api - self.is_bug = is_bug - - -class BugzillaBase(object): - '''An object which represents the data and methods exported by a Bugzilla - instance. Uses xmlrpclib to do its thing. You'll want to create one thusly: - bz=Bugzilla(url='https://bugzilla.redhat.com/xmlrpc.cgi', - user=u, password=p) - - You can get authentication cookies by calling the login() method. These - cookies will be stored in a MozillaCookieJar-style file specified by the - 'cookiefile' attribute (which defaults to ~/.bugzillacookies). Once you - get cookies this way, you will be considered logged in until the cookie - expires. - - You may also specify 'user' and 'password' in a bugzillarc file, either - /etc/bugzillarc or ~/.bugzillarc. The latter will override the former. - The format works like this: - [bugzilla.yoursite.com] - user = username - password = password - You can also use the [DEFAULT] section to set defaults that apply to - any site without a specific section of its own. - Be sure to set appropriate permissions on bugzillarc if you choose to - store your password in it! - - This is an abstract class; it must be implemented by a concrete subclass - which actually connects the methods provided here to the appropriate - methods on the bugzilla instance. - - :kwarg url: base url for the bugzilla instance - :kwarg user: usename to connect with - :kwarg password: password for the connecting user - :kwarg cookiefile: Location to save the session cookies so you don't have - to keep giving the library your username and password. This defaults - to ~/.bugzillacookies. If set to None, the library won't save the - cookies persistently. - ''' - - # bugzilla version that the class is targetting. filled in by - # subclasses - bz_ver_major = 0 - bz_ver_minor = 0 - - # Intended to be the API version of the class, but historically is - # unused and basically worthless since we don't plan on breaking API. - version = "0.1" - - @staticmethod - def url_to_query(url): - ''' - Given a big huge bugzilla query URL, returns a query dict that can - be passed along to the Bugzilla.query() method. - ''' - q = {} - - # pylint: disable=unpacking-non-sequence - (ignore, ignore, path, - ignore, query, ignore) = urlparse(url) - - base = os.path.basename(path) - if base not in ('buglist.cgi', 'query.cgi'): - return {} - - for (k, v) in parse_qsl(query): - if k not in q: - q[k] = v - elif isinstance(q[k], list): - q[k].append(v) - else: - oldv = q[k] - q[k] = [oldv, v] - - # Handle saved searches - if base == "buglist.cgi" and "namedcmd" in q and "sharer_id" in q: - q = { - "sharer_id": q["sharer_id"], - "savedsearch": q["namedcmd"], - } - - return q - - @staticmethod - def fix_url(url): - """ - Turn passed url into a bugzilla XMLRPC web url - """ - if '://' not in url: - log.debug('No scheme given for url, assuming https') - url = 'https://' + url - if url.count('/') < 3: - log.debug('No path given for url, assuming /xmlrpc.cgi') - url = url + '/xmlrpc.cgi' - return url - - def __init__(self, url=None, user=None, password=None, cookiefile=-1, - sslverify=True, tokenfile=-1): - # Hook to allow Bugzilla autodetection without weirdly overriding - # __init__ - if self._init_class_from_url(url, sslverify): - kwargs = locals().copy() - del(kwargs["self"]) - - # pylint: disable=non-parent-init-called - self.__class__.__init__(self, **kwargs) - return - - # Settings the user might want to tweak - self.user = user or '' - self.password = password or '' - self.url = '' - - self._transport = None - self._cookiejar = None - self._sslverify = bool(sslverify) - - self.logged_in = False - self.bug_autorefresh = True - - # Bugzilla object state info that users shouldn't mess with - self._proxy = None - self._products = None - self._bugfields = None - self._components = {} - self._components_details = {} - self._init_private_data() - - if cookiefile == -1: - cookiefile = os.path.expanduser('~/.bugzillacookies') - if tokenfile == -1: - tokenfile = os.path.expanduser("~/.bugzillatoken") - log.debug("Using tokenfile=%s", tokenfile) - self.cookiefile = cookiefile - self.tokenfile = tokenfile - - # List of field aliases. Maps old style RHBZ parameter - # names to actual upstream values. Used for createbug() and - # query include_fields at least. - self._field_aliases = [] - self._add_field_alias('summary', 'short_desc') - self._add_field_alias('description', 'comment') - self._add_field_alias('platform', 'rep_platform') - self._add_field_alias('severity', 'bug_severity') - self._add_field_alias('status', 'bug_status') - self._add_field_alias('id', 'bug_id') - self._add_field_alias('blocks', 'blockedby') - self._add_field_alias('blocks', 'blocked') - self._add_field_alias('depends_on', 'dependson') - self._add_field_alias('creator', 'reporter') - self._add_field_alias('url', 'bug_file_loc') - self._add_field_alias('dupe_of', 'dupe_id') - self._add_field_alias('dupe_of', 'dup_id') - self._add_field_alias('comments', 'longdescs') - self._add_field_alias('creation_time', 'opendate') - self._add_field_alias('creation_time', 'creation_ts') - self._add_field_alias('whiteboard', 'status_whiteboard') - self._add_field_alias('last_change_time', 'delta_ts') - - if url: - self.connect(url) - - def _init_class_from_url(self, url, sslverify): - ignore = url - ignore = sslverify - - def _init_private_data(self): - '''initialize private variables used by this bugzilla instance.''' - self._proxy = None - self._products = None - self._bugfields = None - self._components = {} - self._components_details = {} - - def _get_user_agent(self): - ret = ('Python-urllib bugzilla.py/%s %s' % - (__version__, str(self.__class__.__name__))) - return ret - user_agent = property(_get_user_agent) - - - ################### - # Private helpers # - ################### - - def _check_version(self, major, minor): - """ - Check if the detected bugzilla version is >= passed major/minor pair. - """ - if major < self.bz_ver_major: - return True - if (major == self.bz_ver_major and minor <= self.bz_ver_minor): - return True - return False - - def _listify(self, val): - if val is None: - return val - if type(val) is list: - return val - return [val] - - def _product_id_to_name(self, productid): - '''Convert a product ID (int) to a product name (str).''' - for p in self.products: - if p['id'] == productid: - return p['name'] - raise ValueError('No product with id #%i' % productid) - - def _product_name_to_id(self, product): - '''Convert a product name (str) to a product ID (int).''' - for p in self.products: - if p['name'] == product: - return p['id'] - raise ValueError('No product named "%s"' % product) - - def _add_field_alias(self, *args, **kwargs): - self._field_aliases.append(_FieldAlias(*args, **kwargs)) - - def _get_bug_aliases(self): - return [(f.newname, f.oldname) - for f in self._field_aliases if f.is_bug] - - def _get_api_aliases(self): - return [(f.newname, f.oldname) - for f in self._field_aliases if f.is_api] - - - ################### - # Cookie handling # - ################### - - def _getcookiefile(self): - '''cookiefile is the file that bugzilla session cookies are loaded - and saved from. - ''' - return self._cookiejar.filename - - def _delcookiefile(self): - self._cookiejar = None - - def _setcookiefile(self, cookiefile): - if (self._cookiejar and cookiefile == self._cookiejar.filename): - return - - if self._proxy is not None: - raise RuntimeError("Can't set cookies with an open connection, " - "disconnect() first.") - - log.debug("Using cookiefile=%s", cookiefile) - self._cookiejar = _build_cookiejar(cookiefile) - - cookiefile = property(_getcookiefile, _setcookiefile, _delcookiefile) - - - ############################# - # Login/connection handling # - ############################# - - configpath = ['/etc/bugzillarc', '~/.bugzillarc'] - - def readconfig(self, configpath=None): - ''' - Read bugzillarc file(s) into memory. - ''' - if not configpath: - configpath = self.configpath - configpath = [os.path.expanduser(p) for p in configpath] - c = SafeConfigParser() - r = c.read(configpath) - if not r: - return - # See if we have a config section that matches this url. - section = "" - # Substring match - prefer the longest match found - log.debug("Searching for config section matching %s", self.url) - for s in sorted(c.sections()): - if s in self.url: - log.debug("Found matching section: %s", s) - section = s - if not section: - return - for k, v in c.items(section): - if k in ('user', 'password'): - log.debug("Setting '%s' from configfile", k) - setattr(self, k, v) - - def connect(self, url=None): - ''' - Connect to the bugzilla instance with the given url. - - This will also read any available config files (see readconfig()), - which may set 'user' and 'password'. - - If 'user' and 'password' are both set, we'll run login(). Otherwise - you'll have to login() yourself before some methods will work. - ''' - if url is None and self.url: - url = self.url - url = self.fix_url(url) - - self._transport = RequestsTransport( - url, self._cookiejar, sslverify=self._sslverify) - self._transport.user_agent = self.user_agent - self._proxy = _BugzillaServerProxy(url, self.tokenfile, - self._transport) - - self.url = url - # we've changed URLs - reload config - self.readconfig() - - if (self.user and self.password): - log.info("user and password present - doing login()") - self.login() - - def disconnect(self): - ''' - Disconnect from the given bugzilla instance. - ''' - # clears all the connection state - self._init_private_data() - - - def _login(self, user, password): - '''Backend login method for Bugzilla3''' - return self._proxy.User.login({'login': user, 'password': password}) - - def _logout(self): - '''Backend login method for Bugzilla3''' - return self._proxy.User.logout() - - def login(self, user=None, password=None): - '''Attempt to log in using the given username and password. Subsequent - method calls will use this username and password. Returns False if - login fails, otherwise returns some kind of login info - typically - either a numeric userid, or a dict of user info. It also sets the - logged_in attribute to True, if successful. - - If user is not set, the value of Bugzilla.user will be used. If *that* - is not set, ValueError will be raised. If login fails, BugzillaError - will be raised. - - This method will be called implicitly at the end of connect() if user - and password are both set. So under most circumstances you won't need - to call this yourself. - ''' - if user: - self.user = user - if password: - self.password = password - - if not self.user: - raise ValueError("missing username") - if not self.password: - raise ValueError("missing password") - - try: - ret = self._login(self.user, self.password) - self.logged_in = True - self.password = '' - log.info("login successful for user=%s", self.user) - return ret - except Fault: - e = sys.exc_info()[1] - raise BugzillaError("Login failed: %s" % str(e.faultString)) - - def logout(self): - '''Log out of bugzilla. Drops server connection and user info, and - destroys authentication cookies.''' - self._logout() - self.disconnect() - self.user = '' - self.password = '' - self.logged_in = False - - - ############################################# - # Fetching info about the bugzilla instance # - ############################################# - - def _getbugfields(self): - raise RuntimeError("This bugzilla version does not support listing " - "bug fields.") - - def getbugfields(self, force_refresh=False): - ''' - Calls getBugFields, which returns a list of fields in each bug - for this bugzilla instance. This can be used to set the list of attrs - on the Bug object. - ''' - if force_refresh or self._bugfields is None: - log.debug("Refreshing bugfields") - self._bugfields = self._getbugfields() - self._bugfields.sort() - log.debug("bugfields = %s", self._bugfields) - - return self._bugfields - bugfields = property(fget=lambda self: self.getbugfields(), - fdel=lambda self: setattr(self, '_bugfields', None)) - - - def refresh_products(self, **kwargs): - """ - Refresh a product's cached info - Takes same arguments as _getproductinfo - """ - if self._products is None: - self._products = [] - - for product in self._getproductinfo(**kwargs): - added = False - for current in self._products[:]: - if (current.get("id", -1) != product.get("id", -2) and - current.get("name", -1) != product.get("name", -2)): - continue - - self._products.remove(current) - self._products.append(product) - added = True - break - if not added: - self._products.append(product) - - def getproducts(self, force_refresh=False, **kwargs): - '''Get product data: names, descriptions, etc. - The data varies between Bugzilla versions but the basic format is a - list of dicts, where the dicts will have at least the following keys: - {'id':1, 'name':"Some Product", 'description':"This is a product"} - - Any method that requires a 'product' can be given either the - id or the name.''' - if force_refresh or not self._products: - self._products = self._getproducts(**kwargs) - return self._products - - products = property(fget=lambda self: self.getproducts(), - fdel=lambda self: setattr(self, '_products', None)) - - - def getcomponentsdetails(self, product, force_refresh=False): - '''Returns a dict of dicts, containing detailed component information - for the given product. The keys of the dict are component names. For - each component, the value is a dict with the following keys: - description, initialowner, initialqacontact''' - if force_refresh or product not in self._components_details: - clist = self._getcomponentsdetails(product) - cdict = {} - for item in clist: - name = item['component'] - del item['component'] - cdict[name] = item - self._components_details[product] = cdict - - return self._components_details[product] - - def getcomponentdetails(self, product, component, force_refresh=False): - '''Get details for a single component. Returns a dict with the - following keys: - description, initialowner, initialqacontact, initialcclist''' - d = self.getcomponentsdetails(product, force_refresh) - return d[component] - - def getcomponents(self, product, force_refresh=False): - '''Return a dict of components:descriptions for the given product.''' - if force_refresh or product not in self._components: - self._components[product] = self._getcomponents(product) - return self._components[product] - - def _component_data_convert(self, data, update=False): - if type(data['product']) is int: - data['product'] = self._product_id_to_name(data['product']) - - - # Back compat for the old RH interface - convert_fields = [ - ("initialowner", "default_assignee"), - ("initialqacontact", "default_qa_contact"), - ("initialcclist", "default_cc"), - ] - for old, new in convert_fields: - if old in data: - data[new] = data.pop(old) - - if update: - names = {"product": data.pop("product"), - "component": data.pop("component")} - updates = {} - for k in data.keys(): - updates[k] = data.pop(k) - - data["names"] = [names] - data["updates"] = updates - - - def addcomponent(self, data): - ''' - A method to create a component in Bugzilla. Takes a dict, with the - following elements: - - product: The product to create the component in - component: The name of the component to create - desription: A one sentence summary of the component - default_assignee: The bugzilla login (email address) of the initial - owner of the component - default_qa_contact (optional): The bugzilla login of the - initial QA contact - default_cc: (optional) The initial list of users to be CC'ed on - new bugs for the component. - ''' - data = data.copy() - self._component_data_convert(data) - log.debug("Calling Component.create with: %s", data) - return self._proxy.Component.create(data) - - def editcomponent(self, data): - ''' - A method to edit a component in Bugzilla. Takes a dict, with - mandatory elements of product. component, and initialowner. - All other elements are optional and use the same names as the - addcomponent() method. - ''' - data = data.copy() - self._component_data_convert(data, update=True) - log.debug("Calling Component.update with: %s", data) - return self._proxy.Component.update(data) - - - def _getproductinfo(self, ids=None, names=None, - include_fields=None, exclude_fields=None): - ''' - Get all info for the requested products. - - @ids: List of product IDs to lookup - @names: List of product names to lookup (since bz 4.2, - though we emulate it for older versions) - @include_fields: Only include these fields in the output (since bz 4.2) - @exclude_fields: Do not include these fields in the output (since - bz 4.2) - ''' - if ids is None and names is None: - raise RuntimeError("Products must be specified") - - kwargs = {} - if not self._check_version(4, 2): - if names: - ids = [self._product_name_to_id(name) for name in names] - names = None - include_fields = None - exclude_fields = None - - if ids: - kwargs["ids"] = self._listify(ids) - if names: - kwargs["names"] = self._listify(names) - if include_fields: - kwargs["include_fields"] = include_fields - if exclude_fields: - kwargs["exclude_fields"] = exclude_fields - - # The bugzilla4 name is Product.get(), but Bugzilla3 only had - # Product.get_product, and bz4 kept an alias. - log.debug("Calling Product.get_products with: %s", kwargs) - ret = self._proxy.Product.get_products(kwargs) - return ret['products'] - - def _getproducts(self, **kwargs): - product_ids = self._proxy.Product.get_accessible_products() - r = self._getproductinfo(product_ids['ids'], **kwargs) - return r - - def _getcomponents(self, product): - if type(product) == str: - product = self._product_name_to_id(product) - r = self._proxy.Bug.legal_values({'product_id': product, - 'field': 'component'}) - return r['values'] - - def _getcomponentsdetails(self, product): - # Originally this was a RH extension getProdCompDetails - # Upstream support has been available since 4.2 - if not self._check_version(4, 2): - raise RuntimeError("This bugzilla version does not support " - "fetching component details.") - - comps = None - if self._products is None: - self._products = [] - - def _find_comps(): - for p in self._products: - if p["name"] != product: - continue - return p.get("components", None) - - comps = _find_comps() - if comps is None: - self.refresh_products(names=[product], - include_fields=["name", "id", "components"]) - comps = _find_comps() - - if comps is None: - raise ValueError("Unknown product '%s'" % product) - - # Convert to old style dictionary to maintain back compat - # with original RH bugzilla call - ret = [] - for comp in comps: - row = {} - row["component"] = comp["name"] - row["initialqacontact"] = comp["default_qa_contact"] - row["initialowner"] = comp["default_assigned_to"] - row["description"] = comp["description"] - ret.append(row) - return ret - - - ################### - # getbug* methods # - ################### - - # getbug_extra_fields: Extra fields that need to be explicitly - # requested from Bug.get in order for the data to be returned. This - # decides the difference between getbug() and getbugsimple(). - # - # As of Dec 2012 it seems like only RH bugzilla actually has behavior - # like this, for upstream bz it returns all info for every Bug.get() - _getbug_extra_fields = [] - _supports_getbug_extra_fields = False - - def _getbugs(self, idlist, simple=False, permissive=True, - include_fields=None, exclude_fields=None, extra_fields=None): - ''' - Return a list of dicts of full bug info for each given bug id. - bug ids that couldn't be found will return None instead of a dict. - - @simple: If True, don't ask for any large extra_fields. - ''' - oldidlist = idlist - idlist = [] - for i in oldidlist: - try: - idlist.append(int(i)) - except ValueError: - # String aliases can be passed as well - idlist.append(i) - - extra_fields = self._listify(extra_fields or []) - if not simple: - extra_fields += self._getbug_extra_fields - - getbugdata = {"ids": idlist} - if permissive: - getbugdata["permissive"] = 1 - if self.bz_ver_major >= 4: - if include_fields: - getbugdata["include_fields"] = self._listify(include_fields) - if exclude_fields: - getbugdata["exclude_fields"] = self._listify(exclude_fields) - if self._supports_getbug_extra_fields: - getbugdata["extra_fields"] = extra_fields - - log.debug("Calling Bug.get with: %s", getbugdata) - r = self._proxy.Bug.get(getbugdata) - - if self.bz_ver_major >= 4: - bugdict = dict([(b['id'], b) for b in r['bugs']]) - else: - bugdict = dict([(b['id'], b['internals']) for b in r['bugs']]) - - ret = [] - for i in idlist: - found = None - if i in bugdict: - found = bugdict[i] - else: - # Need to map an alias - for valdict in bugdict.values(): - if i in valdict.get("alias", []): - found = valdict - break - - ret.append(found) - - return ret - - def _getbug(self, objid, simple=False, - include_fields=None, exclude_fields=None, extra_fields=None): - '''Return a dict of full bug info for the given bug id''' - return self._getbugs([objid], simple=simple, permissive=False, - include_fields=include_fields, exclude_fields=exclude_fields, - extra_fields=extra_fields)[0] - - def getbug(self, objid, - include_fields=None, exclude_fields=None, extra_fields=None): - '''Return a Bug object with the full complement of bug data - already loaded.''' - data = self._getbug(objid, include_fields=include_fields, - exclude_fields=exclude_fields, extra_fields=extra_fields) - return _Bug(self, dict=data, autorefresh=self.bug_autorefresh) - - def getbugs(self, idlist, - include_fields=None, exclude_fields=None, extra_fields=None): - '''Return a list of Bug objects with the full complement of bug data - already loaded. If there's a problem getting the data for a given id, - the corresponding item in the returned list will be None.''' - data = self._getbugs(idlist, include_fields=include_fields, - exclude_fields=exclude_fields, extra_fields=extra_fields) - return [(b and _Bug(self, dict=b, - autorefresh=self.bug_autorefresh)) or None - for b in data] - - # Since for so long getbugsimple was just getbug, I don't think we can - # remove any fields without possibly causing a slowdown for some - # existing users. Just have this API mean 'don't ask for the extra - # big stuff' - def getbugsimple(self, objid): - '''Return a Bug object given bug id, populated with simple info''' - return _Bug(self, - dict=self._getbug(objid, simple=True), - autorefresh=self.bug_autorefresh) - - def getbugssimple(self, idlist): - '''Return a list of Bug objects for the given bug ids, populated with - simple info. As with getbugs(), if there's a problem getting the data - for a given bug ID, the corresponding item in the returned list will - be None.''' - return [(b and _Bug(self, dict=b, - autorefresh=self.bug_autorefresh)) or None - for b in self._getbugs(idlist, simple=True)] - - - ################# - # query methods # - ################# - - def _convert_include_field_list(self, _in): - if not _in: - return _in - - for newname, oldname in self._get_api_aliases(): - if oldname in _in: - _in.remove(oldname) - if newname not in _in: - _in.append(newname) - return _in - - def build_query(self, - product=None, - component=None, - version=None, - long_desc=None, - bug_id=None, - short_desc=None, - cc=None, - assigned_to=None, - reporter=None, - qa_contact=None, - status=None, - blocked=None, - dependson=None, - keywords=None, - keywords_type=None, - url=None, - url_type=None, - status_whiteboard=None, - status_whiteboard_type=None, - fixed_in=None, - fixed_in_type=None, - flag=None, - alias=None, - qa_whiteboard=None, - devel_whiteboard=None, - boolean_query=None, - bug_severity=None, - priority=None, - target_milestone=None, - emailtype=None, - booleantype=None, - include_fields=None, - quicksearch=None, - savedsearch=None, - savedsearch_sharer_id=None, - sub_component=None, - tags=None): - """ - Build a query string from passed arguments. Will handle - query parameter differences between various bugzilla versions. - - Most of the parameters should be self explanatory. However - if you want to perform a complex query, and easy way is to - create it with the bugzilla web UI, copy the entire URL it - generates, and pass it to the static method - - Bugzilla.url_to_query - - Then pass the output to Bugzilla.query() - """ - ignore = emailtype - ignore = booleantype - ignore = include_fields - - for key, val in [ - ('fixed_in', fixed_in), - ('blocked', blocked), - ('dependson', dependson), - ('flag', flag), - ('qa_whiteboard', qa_whiteboard), - ('devel_whiteboard', devel_whiteboard), - ('alias', alias), - ('boolean_query', boolean_query), - ('long_desc', long_desc), - ('quicksearch', quicksearch), - ('savedsearch', savedsearch), - ('sharer_id', savedsearch_sharer_id), - ('sub_component', sub_component), - ]: - if val is not None: - raise RuntimeError("'%s' search not supported by this " - "bugzilla" % key) - - query = { - "product": self._listify(product), - "component": self._listify(component), - "version": version, - "id": bug_id, - "short_desc": short_desc, - "bug_status": status, - "keywords": keywords, - "keywords_type": keywords_type, - "bug_file_loc": url, - "bug_file_loc_type": url_type, - "status_whiteboard": status_whiteboard, - "status_whiteboard_type": status_whiteboard_type, - "fixed_in_type": fixed_in_type, - "bug_severity": bug_severity, - "priority": priority, - "target_milestone": target_milestone, - "assigned_to": assigned_to, - "cc": cc, - "qa_contact": qa_contact, - "reporter": reporter, - "tag": self._listify(tags), - } - - # Strip out None elements in the dict - for k, v in query.copy().items(): - if v is None: - del(query[k]) - return query - - def _query(self, query): - # This is kinda redundant now, but various scripts call - # _query with their own assembled dictionaries, so don't - # drop this lest we needlessly break those users - log.debug("Calling Bug.search with: %s", query) - return self._proxy.Bug.search(query) - - def query(self, query): - '''Query bugzilla and return a list of matching bugs. - query must be a dict with fields like those in in querydata['fields']. - Returns a list of Bug objects. - Also see the _query() method for details about the underlying - implementation. - ''' - r = self._query(query) - log.debug("Query returned %s bugs", len(r['bugs'])) - return [_Bug(self, dict=b, - autorefresh=self.bug_autorefresh) for b in r['bugs']] - - def simplequery(self, product, version='', component='', - string='', matchtype='allwordssubstr'): - '''Convenience method - query for bugs filed against the given - product, version, and component whose comments match the given string. - matchtype specifies the type of match to be done. matchtype may be - any of the types listed in querydefaults['long_desc_type_list'], e.g.: - ['allwordssubstr', 'anywordssubstr', 'substring', 'casesubstring', - 'allwords', 'anywords', 'regexp', 'notregexp'] - Return value is the same as with query(). - ''' - q = { - 'product': product, - 'version': version, - 'component': component, - 'long_desc': string, - 'long_desc_type': matchtype - } - return self.query(q) - - def pre_translation(self, query): - '''In order to keep the API the same, Bugzilla4 needs to process the - query and the result. This also applies to the refresh() function - ''' - pass - - def post_translation(self, query, bug): - '''In order to keep the API the same, Bugzilla4 needs to process the - query and the result. This also applies to the refresh() function - ''' - pass - - def bugs_history(self, bug_ids): - ''' - Experimental. Gets the history of changes for - particular bugs in the database. - ''' - return self._proxy.Bug.history({'ids': bug_ids}) - - ####################################### - # Methods for modifying existing bugs # - ####################################### - - # Bug() also has individual methods for many ops, like setassignee() - - def update_bugs(self, ids, updates): - """ - A thin wrapper around bugzilla Bug.update(). Used to update all - values of an existing bug report, as well as add comments. - - The dictionary passed to this function should be generated with - build_update(), otherwise we cannot guarantee back compatibility. - """ - tmp = updates.copy() - tmp["ids"] = self._listify(ids) - - log.debug("Calling Bug.update with: %s", tmp) - return self._proxy.Bug.update(tmp) - - def update_flags(self, idlist, flags): - ''' - Updates the flags associated with a bug report. - Format of flags is: - [{"name": "needinfo", "status": "+", "requestee": "f...@bar.com"}, - {"name": "devel_ack", "status": "-"}, ...] - ''' - d = {"ids": self._listify(idlist), "updates": flags} - log.debug("Calling Flag.update with: %s", d) - return self._proxy.Flag.update(d) - - def update_tags(self, idlist, tags_add=None, tags_remove=None): - ''' - Updates the 'tags' field for a bug. - ''' - tags = {} - if tags_add: - tags["add"] = self._listify(tags_add) - if tags_remove: - tags["remove"] = self._listify(tags_remove) - - d = { - "ids": self._listify(idlist), - "tags": tags, - } - - log.debug("Calling Bug.update_tags with: %s", d) - return self._proxy.Bug.update_tags(d) - - - def build_update(self, - alias=None, - assigned_to=None, - blocks_add=None, - blocks_remove=None, - blocks_set=None, - depends_on_add=None, - depends_on_remove=None, - depends_on_set=None, - cc_add=None, - cc_remove=None, - is_cc_accessible=None, - comment=None, - comment_private=None, - component=None, - deadline=None, - dupe_of=None, - estimated_time=None, - groups_add=None, - groups_remove=None, - keywords_add=None, - keywords_remove=None, - keywords_set=None, - op_sys=None, - platform=None, - priority=None, - product=None, - qa_contact=None, - is_creator_accessible=None, - remaining_time=None, - reset_assigned_to=None, - reset_qa_contact=None, - resolution=None, - see_also_add=None, - see_also_remove=None, - severity=None, - status=None, - summary=None, - target_milestone=None, - target_release=None, - url=None, - version=None, - whiteboard=None, - work_time=None, - fixed_in=None, - qa_whiteboard=None, - devel_whiteboard=None, - internal_whiteboard=None, - sub_component=None): - # pylint: disable=W0221 - # Argument number differs from overridden method - # Base defines it with *args, **kwargs, so we don't have to maintain - # the master argument list in 2 places - ret = {} - - # These are only supported for rhbugzilla - for key, val in [ - ("fixed_in", fixed_in), - ("devel_whiteboard", devel_whiteboard), - ("qa_whiteboard", qa_whiteboard), - ("internal_whiteboard", internal_whiteboard), - ("sub_component", sub_component), - ]: - if val is not None: - raise ValueError("bugzilla instance does not support " - "updating '%s'" % key) - - def s(key, val, convert=None): - if val is None: - return - if convert: - val = convert(val) - ret[key] = val - - def add_dict(key, add, remove, _set=None, convert=None): - if add is remove is _set is None: - return - - def c(val): - val = self._listify(val) - if convert: - val = [convert(v) for v in val] - return val - - newdict = {} - if add is not None: - newdict["add"] = c(add) - if remove is not None: - newdict["remove"] = c(remove) - if _set is not None: - newdict["set"] = c(_set) - ret[key] = newdict - - - s("alias", alias) - s("assigned_to", assigned_to) - s("is_cc_accessible", is_cc_accessible, bool) - s("component", component) - s("deadline", deadline) - s("dupe_of", dupe_of, int) - s("estimated_time", estimated_time, int) - s("op_sys", op_sys) - s("platform", platform) - s("priority", priority) - s("product", product) - s("qa_contact", qa_contact) - s("is_creator_accessible", is_creator_accessible, bool) - s("remaining_time", remaining_time, float) - s("reset_assigned_to", reset_assigned_to, bool) - s("reset_qa_contact", reset_qa_contact, bool) - s("resolution", resolution) - s("severity", severity) - s("status", status) - s("summary", summary) - s("target_milestone", target_milestone) - s("target_release", target_release) - s("url", url) - s("version", version) - s("whiteboard", whiteboard) - s("work_time", work_time, float) - - add_dict("blocks", blocks_add, blocks_remove, blocks_set, - convert=int) - add_dict("depends_on", depends_on_add, depends_on_remove, - depends_on_set, convert=int) - add_dict("cc", cc_add, cc_remove) - add_dict("groups", groups_add, groups_remove) - add_dict("keywords", keywords_add, keywords_remove, keywords_set) - add_dict("see_also", see_also_add, see_also_remove) - - if comment is not None: - ret["comment"] = {"comment": comment} - if comment_private: - ret["comment"]["is_private"] = comment_private - - return ret - - - ######################################## - # Methods for working with attachments # - ######################################## - - def _attachment_uri(self, attachid): - '''Returns the URI for the given attachment ID.''' - att_uri = self.url.replace('xmlrpc.cgi', 'attachment.cgi') - att_uri = att_uri + '?id=%s' % attachid - return att_uri - - def attachfile(self, idlist, attachfile, description, **kwargs): - ''' - Attach a file to the given bug IDs. Returns the ID of the attachment - or raises XMLRPC Fault if something goes wrong. - - attachfile may be a filename (which will be opened) or a file-like - object, which must provide a 'read' method. If it's not one of these, - this method will raise a TypeError. - description is the short description of this attachment. - - Optional keyword args are as follows: - file_name: this will be used as the filename for the attachment. - REQUIRED if attachfile is a file-like object with no - 'name' attribute, otherwise the filename or .name - attribute will be used. - comment: An optional comment about this attachment. - is_private: Set to True if the attachment should be marked private. - is_patch: Set to True if the attachment is a patch. - content_type: The mime-type of the attached file. Defaults to - application/octet-stream if not set. NOTE that text - files will *not* be viewable in bugzilla unless you - remember to set this to text/plain. So remember that! - - Returns the list of attachment ids that were added. If only one - attachment was added, we return the single int ID for back compat - ''' - if isinstance(attachfile, str): - f = open(attachfile) - elif hasattr(attachfile, 'read'): - f = attachfile - else: - raise TypeError("attachfile must be filename or file-like object") - - # Back compat - if "contenttype" in kwargs: - kwargs["content_type"] = kwargs.pop("contenttype") - if "ispatch" in kwargs: - kwargs["is_patch"] = kwargs.pop("ispatch") - if "isprivate" in kwargs: - kwargs["is_private"] = kwargs.pop("isprivate") - if "filename" in kwargs: - kwargs["file_name"] = kwargs.pop("filename") - - kwargs['summary'] = description - - data = f.read() - if not isinstance(data, bytes): - data = data.encode(locale.getpreferredencoding()) - kwargs['data'] = Binary(data) - - kwargs['ids'] = self._listify(idlist) - - if 'file_name' not in kwargs and hasattr(f, "name"): - kwargs['file_name'] = os.path.basename(f.name) - if 'content_type' not in kwargs: - ctype = _detect_filetype(getattr(f, "name", None)) - if not ctype: - ctype = 'application/octet-stream' - kwargs['content_type'] = ctype - - ret = self._proxy.Bug.add_attachment(kwargs) - - if "attachments" in ret: - # Up to BZ 4.2 - ret = [int(k) for k in ret["attachments"].keys()] - elif "ids" in ret: - # BZ 4.4+ - ret = ret["ids"] - - if type(ret) is list and len(ret) == 1: - ret = ret[0] - return ret - - - def openattachment(self, attachid): - '''Get the contents of the attachment with the given attachment ID. - Returns a file-like object.''' - - def get_filename(headers): - import re - - match = re.search( - r'^.*filename="?(.*)"$', - headers.get('content-disposition', '') - ) - - # default to attchid if no match was found - return match.group(1) if match else attachid - - att_uri = self._attachment_uri(attachid) - - response = requests.get(att_uri, cookies=self._cookiejar, stream=True) - - ret = BytesIO() - for chunk in response.iter_content(chunk_size=1024): - if chunk: - ret.write(chunk) - ret.name = get_filename(response.headers) - - # Hooray, now we have a file-like object with .read() and .name - ret.seek(0) - return ret - - def updateattachmentflags(self, bugid, attachid, flagname, **kwargs): - ''' - Updates a flag for the given attachment ID. - Optional keyword args are: - status: new status for the flag ('-', '+', '?', 'X') - requestee: new requestee for the flag - ''' - update = { - 'name': flagname, - 'attach_id': int(attachid), - } - update.update(kwargs.items()) - - result = self._proxy.Flag.update({ - 'ids': [int(bugid)], - 'updates': [update]}) - return result['flag_updates'][str(bugid)] - - - ##################### - # createbug methods # - ##################### - - createbug_required = ('product', 'component', 'summary', 'version', - 'description') - - def build_createbug(self, - product=None, - component=None, - version=None, - summary=None, - description=None, - comment_private=None, - blocks=None, - cc=None, - assigned_to=None, - keywords=None, - depends_on=None, - groups=None, - op_sys=None, - platform=None, - priority=None, - qa_contact=None, - resolution=None, - severity=None, - status=None, - target_milestone=None, - target_release=None, - url=None, - sub_component=None): - - localdict = {} - if blocks: - localdict["blocks"] = self._listify(blocks) - if cc: - localdict["cc"] = self._listify(cc) - if depends_on: - localdict["depends_on"] = self._listify(depends_on) - if groups: - localdict["groups"] = self._listify(groups) - if keywords: - localdict["keywords"] = self._listify(keywords) - if description: - localdict["description"] = description - if comment_private: - localdict["comment_is_private"] = True - - # Most of the machinery and formatting here is the same as - # build_update, so reuse that as much as possible - ret = self.build_update(product=product, component=component, - version=version, summary=summary, op_sys=op_sys, - platform=platform, priority=priority, qa_contact=qa_contact, - resolution=resolution, severity=severity, status=status, - target_milestone=target_milestone, - target_release=target_release, url=url, - assigned_to=assigned_to, sub_component=sub_component) - - ret.update(localdict) - return ret - - def _validate_createbug(self, *args, **kwargs): - # Previous API required users specifying keyword args that mapped - # to the XMLRPC arg names. Maintain that bad compat, but also allow - # receiving a single dictionary like query() does - if kwargs and args: - raise BugzillaError("createbug: cannot specify positional " - "args=%s with kwargs=%s, must be one or the " - "other." % (args, kwargs)) - if args: - if len(args) > 1 or type(args[0]) is not dict: - raise BugzillaError("createbug: positional arguments only " - "accept a single dictionary.") - data = args[0] - else: - data = kwargs - - # If we're getting a call that uses an old fieldname, convert it to the - # new fieldname instead. - for newname, oldname in self._get_api_aliases(): - if (newname in self.createbug_required and - newname not in data and - oldname in data): - data[newname] = data.pop(oldname) - - # Back compat handling for check_args - if "check_args" in data: - del(data["check_args"]) - - return data - - def createbug(self, *args, **kwargs): - ''' - Create a bug with the given info. Returns a new Bug object. - Check bugzilla API documentation for valid values, at least - product, component, summary, version, and description need to - be passed. - ''' - data = self._validate_createbug(*args, **kwargs) - log.debug("Calling Bug.create with: %s", data) - rawbug = self._proxy.Bug.create(data) - return _Bug(self, bug_id=rawbug["id"], - autorefresh=self.bug_autorefresh) - - - ############################## - # Methods for handling Users # - ############################## - - def _getusers(self, ids=None, names=None, match=None): - '''Return a list of users that match criteria. - - :kwarg ids: list of user ids to return data on - :kwarg names: list of user names to return data on - :kwarg match: list of patterns. Returns users whose real name or - login name match the pattern. - :raises XMLRPC Fault: Code 51: if a Bad Login Name was sent to the - names array. - Code 304: if the user was not authorized to see user they - requested. - Code 505: user is logged out and can't use the match or ids - parameter. - - Available in Bugzilla-3.4+ - ''' - params = {} - if ids: - params['ids'] = self._listify(ids) - if names: - params['names'] = self._listify(names) - if match: - params['match'] = self._listify(match) - if not params: - raise BugzillaError('_get() needs one of ids, ' - ' names, or match kwarg.') - - log.debug("Calling User.get with: %s", params) - return self._proxy.User.get(params) - - def getuser(self, username): - '''Return a bugzilla User for the given username - - :arg username: The username used in bugzilla. - :raises XMLRPC Fault: Code 51 if the username does not exist - :returns: User record for the username - ''' - ret = self.getusers(username) - return ret and ret[0] - - def getusers(self, userlist): - '''Return a list of Users from bugzilla. - - :userlist: List of usernames to lookup - :returns: List of User records - ''' - userobjs = [_User(self, **rawuser) for rawuser in - self._getusers(names=userlist).get('users', [])] - - # Return users in same order they were passed in - ret = [] - for u in userlist: - for uobj in userobjs[:]: - if uobj.email == u: - userobjs.remove(uobj) - ret.append(uobj) - break - ret += userobjs - return ret - - - def searchusers(self, pattern): - '''Return a bugzilla User for the given list of patterns - - :arg pattern: List of patterns to match against. - :returns: List of User records - ''' - return [_User(self, **rawuser) for rawuser in - self._getusers(match=pattern).get('users', [])] - - def createuser(self, email, name='', password=''): - '''Return a bugzilla User for the given username - - :arg email: The email address to use in bugzilla - :kwarg name: Real name to associate with the account - :kwarg password: Password to set for the bugzilla account - :raises XMLRPC Fault: Code 501 if the username already exists - Code 500 if the email address isn't valid - Code 502 if the password is too short - Code 503 if the password is too long - :return: User record for the username - ''' - self._proxy.User.create(email, name, password) - return self.getuser(email) - - def updateperms(self, user, action, groups): - ''' - A method to update the permissions (group membership) of a bugzilla - user. - - :arg user: The e-mail address of the user to be acted upon. Can - also be a list of emails. - :arg action: add, remove, or set - :arg groups: list of groups to be added to (i.e. ['fedora_contrib']) - ''' - groups = self._listify(groups) - if action == "rem": - action = "remove" - if action not in ["add", "remove", "set"]: - raise BugzillaError("Unknown user permission action '%s'" % action) - - update = { - "names": self._listify(user), - "groups": { - action: groups, - } - } - - log.debug("Call User.update with: %s", update) - return self._proxy.User.update(update) - - - ###################### - # Deprecated methods # - ###################### - - def initcookiefile(self, cookiefile=None): - ''' - Deprecated: Set self.cookiefile instead. - ''' - if not cookiefile: - cookiefile = os.path.expanduser('~/.bugzillacookies') - self.cookiefile = cookiefile - - - def adduser(self, user, name): - '''Deprecated: Use createuser() instead. - - A method to create a user in Bugzilla. Takes the following: - - user: The email address of the user to create - name: The full name of the user to create - ''' - self.createuser(user, name) - - def getqueryinfo(self, force_refresh=False): - ignore = force_refresh - raise RuntimeError("getqueryinfo is deprecated and the " - "information is not provided by any modern bugzilla.") - querydata = property(getqueryinfo) - querydefaults = property(getqueryinfo) diff --git a/ciabot/bugzilla/bug.py b/ciabot/bugzilla/bug.py deleted file mode 100644 index 80d9720..0000000 --- a/ciabot/bugzilla/bug.py +++ /dev/null @@ -1,517 +0,0 @@ -# base.py - the base classes etc. for a Python interface to bugzilla -# -# Copyright (C) 2007, 2008, 2009, 2010 Red Hat Inc. -# Author: Will Woods <wwo...@redhat.com> -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the -# Free Software Foundation; either version 2 of the License, or (at your -# option) any later version. See http://www.gnu.org/copyleft/gpl.html for -# the full text of the license. - -import locale -import sys - -from bugzilla import log - - -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=True): - # pylint: disable=redefined-builtin - # API had pre-existing issue that we can't change ('dict' usage) - - self.bugzilla = bugzilla - self._bug_fields = [] - self.autorefresh = autorefresh - - if bug_id: - if not dict: - dict = {} - dict["id"] = bug_id - - if dict: - log.debug("Bug(%s)", sorted(dict.keys())) - 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 - - This is available only for compatibility. Using 'str(bug)' and - 'print(bug)' is not recommended because of potential encoding issues. - Please use unicode(bug) where possible. - ''' - if hasattr(sys.version_info, "major") and sys.version_info.major >= 3: - return self.__unicode__() - else: - return self.__unicode__().encode( - locale.getpreferredencoding(), 'replace') - - def __unicode__(self): - '''Return a simple unicode string representation of this bug''' - return u"#%-6s %-10s - %s - %s" % (self.bug_id, self.bug_status, - self.assigned_to, self.summary) - - def __repr__(self): - return '<Bug #%i on %s at %#x>' % (self.bug_id, self.bugzilla.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] - - # pylint: disable=protected-access - aliases = self.bugzilla._get_bug_aliases() - # pylint: enable=protected-access - - for newname, oldname in 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 - - raise AttributeError("Bug object has no attribute '%s'" % name) - - 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 - r = self.bugzilla._getbug(self.bug_id, - include_fields=include_fields, exclude_fields=exclude_fields, - extra_fields=self._bug_fields + (extra_fields or [])) - # pylint: enable=protected-access - self._update_dict(r) - reload = refresh - - def _update_dict(self, newdict): - ''' - Update internal dictionary, in a way that ensures no duplicate - entries are stored WRT field aliases - ''' - if self.bugzilla: - self.bugzilla.post_translation({}, newdict) - - # pylint: disable=protected-access - aliases = self.bugzilla._get_bug_aliases() - # pylint: enable=protected-access - - for newname, oldname in 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]) - - for key in newdict.keys(): - if key not in self._bug_fields: - self._bug_fields.append(key) - 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 = {} - for key in self._bug_fields: - ret[key] = self.__dict__[key] - return ret - - def __setstate__(self, vals): - self._bug_fields = [] - self.bugzilla = None - self._update_dict(vals) - - - ##################### - # Modify bug status # - ##################### - - def setstatus(self, status, comment=None, private=False, - private_in_it=False, nomail=False): - ''' - Update the status for this bug report. - Commonly-used values are ASSIGNED, MODIFIED, and NEEDINFO. - - To change bugs to CLOSED, use .close() instead. - ''' - ignore = private_in_it - ignore = nomail - - 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, - private_in_it=False, nomail=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. - ''' - ignore = private_in_it - ignore = nomail - - vals = self.bugzilla.build_update(comment=comment, - comment_private=isprivate, - resolution=resolution, - dupe_of=dupeid, - fixed_in=fixedin, - status="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, reporter=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='wwo...@redhat.com') - setassignee(qa_contact='wwo...@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 reporter: - raise ValueError("reporter can not be changed") - - 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) - - - ############### - # Add comment # - ############### - - def addcomment(self, comment, private=False, - timestamp=None, worktime=None, bz_gid=None): - ''' - Add the given comment to this bug. Set private to True to mark this - comment as private. - ''' - ignore = timestamp - ignore = bz_gid - ignore = worktime - - 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) - - - ########################## - # Get/set bug whiteboard # - ########################## - - def _dowhiteboard(self, text, which, action, comment, private): - ''' - Update the whiteboard given by 'which' for the given bug. - ''' - if which not in ["status", "qa", "devel", "internal"]: - raise ValueError("Unknown whiteboard type '%s'" % which) - - if not which.endswith('_whiteboard'): - which = which + '_whiteboard' - if which == "status_whiteboard": - which = "whiteboard" - - if action != 'overwrite': - wb = getattr(self, which, '').strip() - tags = wb.split() - - sep = " " - for t in tags: - if t.endswith(","): - sep = ", " - - if action == 'prepend': - text = text + sep + wb - elif action == 'append': - text = wb + sep + text - else: - raise ValueError("Unknown whiteboard action '%s'" % action) - - updateargs = {which: text} - vals = self.bugzilla.build_update(comment=comment, - comment_private=private, - **updateargs) - log.debug("_updatewhiteboard: update=%s", vals) - - self.bugzilla.update_bugs(self.bug_id, vals) - - - def appendwhiteboard(self, text, which='status', - comment=None, private=False): - '''Append the given text (with a space before it) to the given - whiteboard. Defaults to using status_whiteboard.''' - self._dowhiteboard(text, which, "append", comment, private) - - def prependwhiteboard(self, text, which='status', - comment=None, private=False): - '''Prepend the given text (with a space following it) to the given - whiteboard. Defaults to using status_whiteboard.''' - self._dowhiteboard(text, which, "prepend", comment, private) - - def setwhiteboard(self, text, which='status', - comment=None, private=False): - '''Overwrites the contents of the given whiteboard with the given text. - Defaults to using status_whiteboard.''' - self._dowhiteboard(text, which, "overwrite", comment, private) - - def addtag(self, tag, which='status'): - '''Adds the given tag to the given bug.''' - whiteboard = self.getwhiteboard(which) - if whiteboard: - self.appendwhiteboard(tag, which) - else: - self.setwhiteboard(tag, which) - - def gettags(self, which='status'): - '''Get a list of tags (basically just whitespace-split the given - whiteboard)''' - return self.getwhiteboard(which).split() - - def deltag(self, tag, which='status'): - '''Removes the given tag from the given bug.''' - tags = self.gettags(which) - for t in tags: - if t.strip(",") == tag: - tags.remove(t) - self.setwhiteboard(' '.join(tags), which) - - - ##################### - # 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'] - - - ######################## - # Experimental methods # - ######################## - - def get_history(self): - ''' - Experimental. Get the history of changes for this bug. - ''' - return self.bugzilla.bugs_history([self.bug_id]) - - ###################### - # Deprecated methods # - ###################### - - def getwhiteboard(self, which='status'): - ''' - Deprecated. Use bug.qa_whiteboard, bug.devel_whiteboard, etc. - ''' - return getattr(self, "%s_whiteboard" % which) - - def updateflags(self, flags): - ''' - Deprecated, use bugzilla.update_flags() directly - ''' - flaglist = [] - for key, value in flags.items(): - flaglist.append({"name": key, "status": value}) - return self.bugzilla.update_flags(self.bug_id, flaglist) - - -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) diff --git a/ciabot/bugzilla/bugzilla3.py b/ciabot/bugzilla/bugzilla3.py deleted file mode 100644 index efacdea..0000000 --- a/ciabot/bugzilla/bugzilla3.py +++ /dev/null @@ -1,34 +0,0 @@ -# bugzilla3.py - a Python interface to Bugzilla 3.x using xmlrpclib. -# -# Copyright (C) 2008, 2009 Red Hat Inc. -# Author: Will Woods <wwo...@redhat.com> -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the -# Free Software Foundation; either version 2 of the License, or (at your -# option) any later version. See http://www.gnu.org/copyleft/gpl.html for -# the full text of the license. - -from bugzilla.base import BugzillaBase - - -class Bugzilla3(BugzillaBase): - bz_ver_major = 3 - bz_ver_minor = 0 - - -class Bugzilla32(Bugzilla3): - bz_ver_minor = 2 - - -class Bugzilla34(Bugzilla32): - bz_ver_minor = 4 - - -class Bugzilla36(Bugzilla34): - bz_ver_minor = 6 - - def _getbugfields(self): - '''Get the list of valid fields for Bug objects''' - r = self._proxy.Bug.fields({'include_fields': ['name']}) - return [f['name'] for f in r['fields']] diff --git a/ciabot/bugzilla/bugzilla4.py b/ciabot/bugzilla/bugzilla4.py deleted file mode 100644 index 7f5e127..0000000 --- a/ciabot/bugzilla/bugzilla4.py +++ /dev/null @@ -1,47 +0,0 @@ -# -# Copyright (C) 2008-2012 Red Hat Inc. -# Author: Michal Novotny <minov...@redhat.com> -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the -# Free Software Foundation; either version 2 of the License, or (at your -# option) any later version. See http://www.gnu.org/copyleft/gpl.html for -# the full text of the license. - -from bugzilla.bugzilla3 import Bugzilla36 - - -class Bugzilla4(Bugzilla36): - bz_ver_major = 4 - bz_ver_minor = 0 - - - ################# - # Query Methods # - ################# - - def build_query(self, **kwargs): - query = Bugzilla36.build_query(self, **kwargs) - - # 'include_fields' only available for Bugzilla4+ - include_fields = self._convert_include_field_list( - kwargs.pop('include_fields', None)) - if include_fields: - if 'id' not in include_fields: - include_fields.append('id') - query["include_fields"] = include_fields - - exclude_fields = self._convert_include_field_list( - kwargs.pop('exclude_fields', None)) - if exclude_fields: - query["exclude_fields"] = exclude_fields - - return query - - -class Bugzilla42(Bugzilla4): - bz_ver_minor = 2 - - -class Bugzilla44(Bugzilla42): - bz_ver_minor = 4 diff --git a/ciabot/bugzilla/rhbugzilla.py b/ciabot/bugzilla/rhbugzilla.py deleted file mode 100644 index 4c6c7e6..0000000 --- a/ciabot/bugzilla/rhbugzilla.py +++ /dev/null @@ -1,368 +0,0 @@ -# rhbugzilla.py - a Python interface to Red Hat Bugzilla using xmlrpclib. -# -# Copyright (C) 2008-2012 Red Hat Inc. -# Author: Will Woods <wwo...@redhat.com> -# -# This program is free software; you can redistribute it and/or modify it -# under the terms of the GNU General Public License as published by the -# Free Software Foundation; either version 2 of the License, or (at your -# option) any later version. See http://www.gnu.org/copyleft/gpl.html for -# the full text of the license. - - -from bugzilla import log -from bugzilla.bugzilla4 import Bugzilla44 as _parent - - -class RHBugzilla(_parent): - ''' - Bugzilla class for connecting Red Hat's forked bugzilla instance, - bugzilla.redhat.com - - Historically this class used many more non-upstream methods, but - in 2012 RH started dropping most of its custom bits. By that time, - upstream BZ had most of the important functionality. - - Much of the remaining code here is just trying to keep things operating - in python-bugzilla back compatible manner. - - This class was written using bugzilla.redhat.com's API docs: - https://bugzilla.redhat.com/docs/en/html/api/ - ''' - - def __init__(self, *args, **kwargs): - """ - @rhbz_back_compat: If True, convert parameters to the format they were - in prior RHBZ upgrade in June 2012. Mostly this replaces lists - with comma separated strings, and alters groups and flags. - Default is False. Please don't use this in new code, just update - your scripts. - @multicall: Unused nowadays, will be removed in the future - """ - # 'multicall' is no longer used, just ignore it - multicall = kwargs.pop("multicall", None) - self.rhbz_back_compat = bool(kwargs.pop("rhbz_back_compat", False)) - - if multicall is not None: - log.warn("multicall is unused and will be removed in a " - "future release.") - - if self.rhbz_back_compat: - log.warn("rhbz_back_compat will be removed in a future release.") - - _parent.__init__(self, *args, **kwargs) - - def _add_both_alias(newname, origname): - self._add_field_alias(newname, origname, is_api=False) - self._add_field_alias(origname, newname, is_bug=False) - - _add_both_alias('fixed_in', 'cf_fixed_in') - _add_both_alias('qa_whiteboard', 'cf_qa_whiteboard') - _add_both_alias('devel_whiteboard', 'cf_devel_whiteboard') - _add_both_alias('internal_whiteboard', 'cf_internal_whiteboard') - - self._add_field_alias('component', 'components', is_bug=False) - self._add_field_alias('version', 'versions', is_bug=False) - self._add_field_alias('sub_component', 'sub_components', is_bug=False) - - # flags format isn't exactly the same but it's the closest approx - self._add_field_alias('flags', 'flag_types') - - self._getbug_extra_fields = self._getbug_extra_fields + [ - "comments", "description", - "external_bugs", "flags", "sub_components", - "tags", - ] - self._supports_getbug_extra_fields = True - - - ###################### - # Bug update methods # - ###################### - - def build_update(self, **kwargs): - adddict = {} - - def pop(key, destkey): - val = kwargs.pop(key, None) - if val is None: - return - adddict[destkey] = val - - def get_sub_component(): - val = kwargs.pop("sub_component", None) - if val is None: - return - - if type(val) is not dict: - component = self._listify(kwargs.get("component")) - if not component: - raise ValueError("component must be specified if " - "specifying sub_component") - val = {component[0]: val} - adddict["sub_components"] = val - - pop("fixed_in", "cf_fixed_in") - pop("qa_whiteboard", "cf_qa_whiteboard") - pop("devel_whiteboard", "cf_devel_whiteboard") - pop("internal_whiteboard", "cf_internal_whiteboard") - - get_sub_component() - - vals = _parent.build_update(self, **kwargs) - vals.update(adddict) - - return vals - - - ################# - # Query methods # - ################# - - def pre_translation(self, query): - '''Translates the query for possible aliases''' - old = query.copy() - - if 'bug_id' in query: - if type(query['bug_id']) is not list: - query['id'] = query['bug_id'].split(',') - else: - query['id'] = query['bug_id'] - del query['bug_id'] - - if 'component' in query: - if type(query['component']) is not list: - query['component'] = query['component'].split(',') - - if 'include_fields' not in query and 'column_list' not in query: - return - - if 'include_fields' not in query: - query['include_fields'] = [] - if 'column_list' in query: - query['include_fields'] = query['column_list'] - del query['column_list'] - - # We need to do this for users here for users that - # don't call build_query - self._convert_include_field_list(query['include_fields']) - - if old != query: - log.debug("RHBugzilla pretranslated query to: %s", query) - - def post_translation(self, query, bug): - ''' - Convert the results of getbug back to the ancient RHBZ value - formats - ''' - ignore = query - - # RHBZ _still_ returns component and version as lists, which - # deviates from upstream. Copy the list values to components - # and versions respectively. - if 'component' in bug and "components" not in bug: - val = bug['component'] - bug['components'] = type(val) is list and val or [val] - bug['component'] = bug['components'][0] - - if 'version' in bug and "versions" not in bug: - val = bug['version'] - bug['versions'] = type(val) is list and val or [val] - bug['version'] = bug['versions'][0] - - # sub_components isn't too friendly of a format, add a simpler - # sub_component value - if 'sub_components' in bug and 'sub_component' not in bug: - val = bug['sub_components'] - bug['sub_component'] = "" - if type(val) is dict: - values = [] - for vallist in val.values(): - values += vallist - bug['sub_component'] = " ".join(values) - - if not self.rhbz_back_compat: - return - - if 'flags' in bug and type(bug["flags"]) is list: ... etc. - the rest is truncated _______________________________________________ Libreoffice-commits mailing list libreoffice-comm...@lists.freedesktop.org https://lists.freedesktop.org/mailman/listinfo/libreoffice-commits