PkgInfo: Code cleanup & PEP8 compliancy

- Use supybot.utils.str.format() for correct pluralization of 'results'
- Better code readability
This commit is contained in:
James Lu 2014-12-28 22:45:11 -08:00
parent f1f8c6176e
commit 82b353a075

View File

@ -45,8 +45,8 @@ try:
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
except ImportError: except ImportError:
raise ImportError("Beautiful Soup 4 is required for this plugin: get it" raise ImportError("Beautiful Soup 4 is required for this plugin: get it"
" at http://www.crummy.com/software/BeautifulSoup/bs4/doc/" " at http://www.crummy.com/software/BeautifulSoup/bs4/"
"#installing-beautiful-soup") "doc/#installing-beautiful-soup")
try: try:
from supybot.i18n import PluginInternationalization from supybot.i18n import PluginInternationalization
@ -54,7 +54,8 @@ try:
except ImportError: except ImportError:
# Placeholder that allows to run the plugin on a bot # Placeholder that allows to run the plugin on a bot
# without the i18n module # without the i18n module
_ = lambda x:x _ = lambda x: x
class PkgInfo(callbacks.Plugin): class PkgInfo(callbacks.Plugin):
"""Fetches package information from the repositories of """Fetches package information from the repositories of
@ -64,31 +65,35 @@ class PkgInfo(callbacks.Plugin):
def __init__(self, irc): def __init__(self, irc):
self.__parent = super(PkgInfo, self) self.__parent = super(PkgInfo, self)
self.__parent.__init__(irc) self.__parent.__init__(irc)
self.addrs = {'ubuntu':'http://packages.ubuntu.com/', self.addrs = {'ubuntu': 'http://packages.ubuntu.com/',
'debian':"https://packages.debian.org/"} 'debian': "https://packages.debian.org/"}
def _getDistro(self, release): def _getDistro(self, release):
"""<release> """<release>
Guesses the distribution from the release name.""" Guesses the distribution from the release name."""
release = release.lower() release = release.lower()
if release.startswith(("oldstable","squeeze","wheezy","stable", if release.startswith(("oldstable", "squeeze", "wheezy", "stable",
"jessie","testing","sid","unstable")): "jessie", "testing", "sid", "unstable")):
distro = "debian" distro = "debian"
elif release.startswith(("hardy","lucid","maverick","natty","oneiric", elif release.startswith(("hardy", "lucid", "maverick", "natty",
"precise","quantal","raring","saucy","trusty","utopic","vivid")): "oneiric", "precise", "quantal", "raring",
"saucy", "trusty", "utopic", "vivid")):
distro = "ubuntu" distro = "ubuntu"
else: else:
distro = None distro = None
return distro return distro
def MadisonParse(self, pkg, dist, codenames='', suite='', useSource=False, reverse=False): def MadisonParse(self, pkg, dist, codenames='', suite='', useSource=False,
# The arch value implies 'all' (architecture-independent packages) and 'source' reverse=False):
# (source packages), in order to prevent misleading "Not found" errors. """Parser for the madison API at https://qa.debian.org/madison.php."""
# This arch value implies 'all' (architecture-independent packages)
# and 'source' (source packages), in order to prevent misleading
# "Not found" errors.
arch = self.registryValue("archs") + ['all', 'source'] arch = self.registryValue("archs") + ['all', 'source']
arch = ','.join(set(arch)) arch = ','.join(set(arch))
self.arg = {'package':pkg,'table':dist,'a':arch,'c':codenames,'s':suite, self.arg = {'package': pkg, 'table': dist, 'a': arch, 'c': codenames,
} 's': suite}
if useSource: if useSource:
self.arg['S'] = 'on' self.arg['S'] = 'on'
self.arg = urlencode(self.arg) self.arg = urlencode(self.arg)
@ -98,21 +103,24 @@ class PkgInfo(callbacks.Plugin):
fd = utils.web.getUrlFd(url) fd = utils.web.getUrlFd(url)
for line in fd.readlines(): for line in fd.readlines():
L = line.decode("utf-8").split("|") L = line.decode("utf-8").split("|")
L = list(map(str.strip, list(map(str, L)))) L = map(str.strip, L)
name, version, release, archs = L
if useSource: if useSource:
d['%s: %s' % (L[2], L[0])] = (L[1], L[3]) d['%s: %s' % (release, name)] = (version, archs)
else: else:
d[L[2]] = (L[1],L[3]) d[release] = (version, archs)
if d: if d:
if reverse: if reverse:
# *sigh*... I wish there was a better way to do this # *sigh*... I wish there was a better way to do this
d = OrderedDict(reversed(tuple(d.items()))) d = OrderedDict(reversed(tuple(d.items())))
if self.registryValue("verbose"): if self.registryValue("verbose"):
return 'Found %s results: ' % len(d) + ', '.join("{!s} " \ items = ["{name} \x02({version} [{archs}])\x02".format(name=k,
"\x02({!s} [{!s}])\x02".format(k,v[0],v[1]) for (k,v) in \ version=v[0], archs=v[1]) for (k, v) in d.items()]
d.items()) else:
return 'Found %s results: ' % len(d) + ', '.join("{!s} " \ items = ["{name} \x02({version})\x02".format(name=k,
"\x02({!s})\x02".format(k,v[0]) for (k,v) in d.items()) version=v[0]) for (k, v) in d.items()]
s = format('Found %n: %L', (len(d), 'result'), items)
return s
else: else:
self.log.debug("PkgInfo: No results found for URL %s", url) self.log.debug("PkgInfo: No results found for URL %s", url)
@ -121,8 +129,8 @@ class PkgInfo(callbacks.Plugin):
Fetches information for <package> from Debian or Ubuntu's repositories. Fetches information for <package> from Debian or Ubuntu's repositories.
<release> is the codename/release name (e.g. 'trusty', 'squeeze'). If <release> is the codename/release name (e.g. 'trusty', 'squeeze'). If
--depends, --recommends, or --suggests is given, fetches dependency info --depends, --recommends, or --suggests is given, fetches dependency
for <package>. info for <package>.
For Arch Linux packages, please use 'archpkg' and 'archaur' instead.""" For Arch Linux packages, please use 'archpkg' and 'archaur' instead."""
pkg = pkg.lower() pkg = pkg.lower()
distro = self._getDistro(release) distro = self._getDistro(release)
@ -136,7 +144,7 @@ class PkgInfo(callbacks.Plugin):
irc.error(str(e), Raise=True) irc.error(str(e), Raise=True)
soup = BeautifulSoup(fd) soup = BeautifulSoup(fd)
if "Error" in soup.title.string: if "Error" in soup.title.string:
err = soup.find('div', attrs={"id":"content"}).find('p').string err = soup.find('div', attrs={"id": "content"}).find('p').string
if "two or more packages specified" in err: if "two or more packages specified" in err:
irc.error("Unknown distribution/release.", Raise=True) irc.error("Unknown distribution/release.", Raise=True)
irc.reply(err) irc.reply(err)
@ -144,7 +152,8 @@ class PkgInfo(callbacks.Plugin):
opts = dict(opts) opts = dict(opts)
if opts: if opts:
items = soup.find_all('dt') items = soup.find_all('dt')
keyws = {'depends': 'dep:', 'recommends': 'rec:', 'suggests': 'sug:'} keyws = {'depends': 'dep:', 'recommends': 'rec:',
'suggests': 'sug:'}
if 'depends' in opts: if 'depends' in opts:
lookup = 'depends' lookup = 'depends'
elif 'recommends' in opts: elif 'recommends' in opts:
@ -164,27 +173,28 @@ class PkgInfo(callbacks.Plugin):
except AttributeError as e: except AttributeError as e:
continue continue
if res: if res:
s = format("Package \x02%s\x02 %s: %L, View more at %u", pkg, lookup, s = format("Package \x02%s\x02 %s: %L, View more at %u", pkg,
res, url) lookup, res, url)
irc.reply(s) irc.reply(s)
else: else:
irc.error("%s doesn't seem to have any %s." % (pkg, lookup)) irc.error("%s doesn't seem to have any %s." % (pkg, lookup))
return return
desc = soup.find('meta', attrs={"name":"Description"})["content"] desc = soup.find('meta', attrs={"name": "Description"})["content"]
# Get package information from the meta tags # Get package information from the meta tags
keywords = soup.find('meta', attrs={"name":"Keywords"})["content"] keywords = soup.find('meta', attrs={"name": "Keywords"})["content"]
keywords = keywords.replace(",","").split() keywords = keywords.replace(",", "").split()
version = keywords[-1] version = keywords[-1]
if version == "virtual": if version == "virtual":
providing = [obj.a.text for obj in soup.find_all('dt')] providing = [obj.a.text for obj in soup.find_all('dt')]
desc = "Virtual package provided by: \x02%s\x02" % ', '.join(providing[:10]) desc = ("Virtual package provided by: \x02%s\x02" %
', '.join(providing[:10]))
if len(providing) > 10: if len(providing) > 10:
desc += " and \x02%s\x02 others" % (len(providing) - 10) desc += " and \x02%s\x02 others" % (len(providing) - 10)
s = format("Package: \x02%s (%s)\x02 in %s - %s, View more at: %u", pkg, s = format("Package: \x02%s (%s)\x02 in %s - %s, View more at: %u",
version, keywords[1], desc, url) pkg, version, keywords[1], desc, url)
irc.reply(s) irc.reply(s)
pkg = wrap(package, ['somethingWithoutSpaces', 'somethingWithoutSpaces', pkg = wrap(package, ['somethingWithoutSpaces', 'somethingWithoutSpaces',
getopts({'depends':'', 'recommends':'', 'suggests':''})]) getopts({'depends': '', 'recommends': '', 'suggests': ''})])
def vlist(self, irc, msg, args, distro, pkg, opts): def vlist(self, irc, msg, args, distro, pkg, opts):
"""<distribution> <package> [--source] [--reverse] """<distribution> <package> [--source] [--reverse]
@ -203,16 +213,18 @@ class PkgInfo(callbacks.Plugin):
irc.error("Unknown distribution.", Raise=True) irc.error("Unknown distribution.", Raise=True)
opts = dict(opts) opts = dict(opts)
reverse = 'reverse' in opts reverse = 'reverse' in opts
d = self.MadisonParse(pkg, distro, useSource='source' in opts, reverse=reverse) d = self.MadisonParse(pkg, distro, useSource='source' in opts,
if not d: irc.error("No results found.",Raise=True) reverse=reverse)
if not d:
irc.error("No results found.", Raise=True)
try: try:
url = "{}search?keywords={}".format(self.addrs[distro], pkg) url = "{}search?keywords={}".format(self.addrs[distro], pkg)
d += format(" View more at: %u", url) d += format(" View more at: %u", url)
except KeyError: except KeyError:
pass pass
irc.reply(d) irc.reply(d)
vlist = wrap(vlist, ['somethingWithoutSpaces', 'somethingWithoutSpaces', getopts({'source':'', vlist = wrap(vlist, ['somethingWithoutSpaces', 'somethingWithoutSpaces',
'reverse':''})]) getopts({'source': '', 'reverse': ''})])
def archpkg(self, irc, msg, args, pkg, opts): def archpkg(self, irc, msg, args, pkg, opts):
"""<package> [--exact] """<package> [--exact]
@ -223,27 +235,30 @@ class PkgInfo(callbacks.Plugin):
pkg = pkg.lower() pkg = pkg.lower()
baseurl = 'https://www.archlinux.org/packages/search/json/?' baseurl = 'https://www.archlinux.org/packages/search/json/?'
if 'exact' in dict(opts): if 'exact' in dict(opts):
url = baseurl + urlencode({'name':pkg}) url = baseurl + urlencode({'name': pkg})
else: else:
url = baseurl + urlencode({'q':pkg}) url = baseurl + urlencode({'q': pkg})
self.log.debug("PkgInfo: using url %s for 'archpkg' command", url) self.log.debug("PkgInfo: using url %s for 'archpkg' command", url)
fd = utils.web.getUrl(url) fd = utils.web.getUrl(url)
data = json.loads(fd.decode("utf-8")) data = json.loads(fd.decode("utf-8"))
if data['valid'] and data['results']: if data['valid'] and data['results']:
f = set() # We want one entry per package, but the API gives one
# entry per architecture! Remove duplicates with a set:
results = set()
archs = defaultdict(list) archs = defaultdict(list)
for x in data['results']: for x in data['results']:
s = "{} - {} \x02({})\x02".format(x['pkgname'],x['pkgdesc'],x['pkgver']) s = "\x02{name}\x02 - {desc} \x02({version})\x02".format(
f.add(s) name=x['pkgname'], desc=x['pkgdesc'], version=x['pkgver'])
results.add(s)
archs[s].append(x['arch']) archs[s].append(x['arch'])
count = len(f) count = len(results)
if self.registryValue("verbose"): items = [format("%s \x02[%s]\x02", s, ', '.join(archs[s])) for s
irc.reply('Found %s results: ' % count + ', ' \ in results]
.join("{} \x02[{!s}]\x02".format(s, ', '.join(archs[s])) for s in f)) irc.reply(format('Found %n: %L', (len(results), 'result'),
list(results)))
else: else:
irc.reply('Found {} results: {}'.format(count,', '.join(f))) irc.error("No results found.", Raise=True)
else: irc.error("No results found.",Raise=True) archpkg = wrap(archpkg, ['somethingWithoutSpaces', getopts({'exact': ''})])
archpkg = wrap(archpkg, ['somethingWithoutSpaces', getopts({'exact':''})])
def archaur(self, irc, msg, args, pkg): def archaur(self, irc, msg, args, pkg):
"""<package> """<package>
@ -251,7 +266,7 @@ class PkgInfo(callbacks.Plugin):
Looks up <package> in the Arch Linux AUR.""" Looks up <package> in the Arch Linux AUR."""
pkg = pkg.lower() pkg = pkg.lower()
baseurl = 'https://aur.archlinux.org/rpc.php?type=search&' baseurl = 'https://aur.archlinux.org/rpc.php?type=search&'
url = baseurl + urlencode({'arg':pkg}) url = baseurl + urlencode({'arg': pkg})
self.log.debug("PkgInfo: using url %s for 'archaur' command", url) self.log.debug("PkgInfo: using url %s for 'archaur' command", url)
fd = utils.web.getUrl(url) fd = utils.web.getUrl(url)
data = json.loads(fd.decode("utf-8")) data = json.loads(fd.decode("utf-8"))
@ -263,13 +278,15 @@ class PkgInfo(callbacks.Plugin):
# in the bot. # in the bot.
if count > 150: if count > 150:
count = '150+' count = '150+'
s = "Found {} result{}: ".format(count, s = format("Found %n: ", (data["resultcount"], 'result'))
's' if data["resultcount"] != 1 else '')
for x in data['results'][:150]: for x in data['results'][:150]:
verboseInfo = '' verboseInfo = ''
if self.registryValue("verbose"): if self.registryValue("verbose"):
verboseInfo = " [ID:{} Votes:{}]".format(x['ID'], x['NumVotes']) verboseInfo = format("[ID: %s Votes: %s]", x['ID'],
s += "{} - {} \x02({}{})\x02, ".format(x['Name'],x['Description'],x['Version'], verboseInfo) x['NumVotes'])
s += "{name} - {desc} \x02({version} {verbose})\x02, " \
.format(name=x['Name'], desc=x['Description'],
version=x['Version'], verbose=verboseInfo)
irc.reply(s[:-2]) # cut off the ", " at the end irc.reply(s[:-2]) # cut off the ", " at the end
else: else:
irc.error("No results found.", Raise=True) irc.error("No results found.", Raise=True)
@ -294,30 +311,36 @@ class PkgInfo(callbacks.Plugin):
# Debian/Ubuntu use h3 for result names in the format 'Package abcd' # Debian/Ubuntu use h3 for result names in the format 'Package abcd'
results = [pkg.string.split()[1] for pkg in soup.find_all('h3')] results = [pkg.string.split()[1] for pkg in soup.find_all('h3')]
if results: if results:
s = format("Found %s results: \x02%L\x02, View more at: %u", len(results), s = format("Found %n: \x02%L\x02, View more at: %u",
results, url) (len(results), 'result'), results, url)
irc.reply(s) irc.reply(s)
else: else:
e = "No results found."
try: try:
# Look for "too many results" errors and others reported by the
# web interface.
if distro == "debian": if distro == "debian":
errorParse = soup.find("div", class_="note").p errorParse = soup.find("div", class_="note").p
else: else:
errorParse = soup.find("p", attrs={"id": "psearchtoomanyhits"}) errorParse = soup.find("p", attrs={"id":
"psearchtoomanyhits"})
if errorParse: if errorParse:
for br in errorParse.findAll('br'): for br in errorParse.findAll('br'):
br.replace_with(" ") br.replace_with(" ")
e = errorParse.text.strip() e = errorParse.text.strip()
except AttributeError: except AttributeError:
pass e = "No results found."
irc.error(e) irc.error(e)
pkgsearch = wrap(pkgsearch, ['somethingWithoutSpaces', 'somethingWithoutSpaces']) pkgsearch = wrap(pkgsearch, ['somethingWithoutSpaces',
'somethingWithoutSpaces'])
def mintpkg(self, irc, msg, args, release, query, opts): def mintpkg(self, irc, msg, args, release, query, opts):
"""<release> <package> [--exact] """<release> <package> [--exact]
Looks up <package> in Linux Mint's repositories.""" Looks up <package> in Linux Mint's repositories. If --exact is given,
addr = 'http://packages.linuxmint.com/list.php?release=' + quote(release) look up packages by the exact package name. Otherwise, look it up
as a simple glob pattern."""
addr = 'http://packages.linuxmint.com/list.php?release=' + \
quote(release)
try: try:
fd = utils.web.getUrl(addr).decode("utf-8") fd = utils.web.getUrl(addr).decode("utf-8")
except utils.web.Error as e: except utils.web.Error as e:
@ -336,21 +359,26 @@ class PkgInfo(callbacks.Plugin):
# Ascend to find the section name (in <h2>): # Ascend to find the section name (in <h2>):
section = result.parent.parent.parent.previous_sibling.\ section = result.parent.parent.parent.previous_sibling.\
previous_sibling.string previous_sibling.string
# Find the package version in the next <td>; for some reason we have # Find the package version in the next <td>; for some reason we
# to go two siblings further, as the first .next_sibling returns '\n'. # have to go two siblings further, as the first .next_sibling
# This is mentioned briefly in Beautiful Soup 4's documentation... # returns '\n'. This is mentioned briefly in Beautiful Soup 4's
# documentation...
version = result.next_sibling.next_sibling.string version = result.next_sibling.next_sibling.string
# We format our found dictionary this way because the same
# package can exist multiple times in different sections of
# the repository (e.g. one in Main, one in Backports, etc.)
found['%s [\x02%s\x02]' % (name, section)] = version found['%s [\x02%s\x02]' % (name, section)] = version
if found: if found:
s = 'Found %s results: ' % len(found) items = [format('%s \x02(%s)\x02', pkg, found[pkg]) for pkg in
for x in found: found]
s += '%s \x02(%s)\x02, ' % (x, found[x]) s = format('Found %n: %L, %s %u', (len(found), 'result'), items,
s += format('View more at: %u', addr) _('View more at: '), addr)
irc.reply(s) irc.reply(s)
else: else:
irc.error('No results found.') irc.error('No results found.')
mintpkg = wrap(mintpkg, ['somethingWithoutSpaces', 'somethingWithoutSpaces', mintpkg = wrap(mintpkg, ['somethingWithoutSpaces',
getopts({'exact':''})]) 'somethingWithoutSpaces',
getopts({'exact': ''})])
Class = PkgInfo Class = PkgInfo