plugin_pack.py
author John Bailey <rekkanoryo@rekkanoryo.org>
Sun Aug 30 20:10:58 2009 -0400 (2009-08-30)
changeset 1046 93089a7ce7f6
parent 966 770d26fae93e
permissions -rwxr-xr-x
Merge
     1 #!/usr/bin/python
     2 
     3 # plugin_pack.py - Helper script for obtaining info about the plugin pack
     4 # Copyright (C) 2008 Gary Kramlich <grim@reaperworld.com>
     5 #
     6 # This program is free software; you can redistribute it and/or
     7 # modify it under the terms of the GNU General Public License
     8 # as published by the Free Software Foundation; either version 2
     9 # of the License, or (at your option) any later version.
    10 #
    11 # This program is distributed in the hope that it will be useful,
    12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
    13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14 # GNU General Public License for more details.
    15 #
    16 # You should have received a copy of the GNU General Public License
    17 # along with this program; if not, write to the Free Software
    18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301, USA.
    19 
    20 """Usage: plugin_pack.py [OPTION...] command
    21 
    22 Flags:
    23 
    24   -a  Load abusive plugins
    25   -d  Load default plugins
    26   -i  Load incomplate plugins
    27 
    28   -f  Load finch plugins
    29   -p  Load purple plugins
    30   -P  Load pidgin plugins
    31 
    32 Commands:
    33 """
    34 
    35 import ConfigParser
    36 import getopt
    37 import glob
    38 import os.path
    39 import string
    40 import sys
    41 
    42 webpage = 'http://plugins.guifications.org/'
    43 
    44 def printerr(msg):
    45 	print >> sys.stderr, msg
    46 
    47 class Plugin:
    48 	name = ''
    49 	directory = ''
    50 	type = ''
    51 	depends = []
    52 	provides = ''
    53 	summary = ''
    54 	description = ''
    55 	authors = []
    56 	introduced = ''
    57 	notes = ''
    58 
    59 	def __init__(self, directory, name, parser):
    60 		self.name = name
    61 
    62 		self.directory = directory
    63 
    64 		self.type = parser.get(name, 'type')
    65 		self.depends = parser.get(name, 'depends').split()
    66 		self.provides = parser.get(name, 'provides')
    67 		self.summary = parser.get(name, 'summary')
    68 		self.description = parser.get(name, 'description')
    69 		self.authors = parser.get(name, 'authors').split(',')
    70 		self.introduced = parser.get(name, 'introduced')
    71 
    72 		if parser.has_option(name, 'notes'):
    73 			self.notes = parser.get(name, 'notes')
    74 
    75 		if self.type != 'default' and self.type != 'incomplete' and self.type != 'abusive':
    76 			printerr('\'%s\' has an unknown type of \'%s\'!' % (self.name, self.type))
    77 
    78 	def __str__(self):
    79 		output  = 'name: %s\n' % self.name
    80 		output += 'authors: %s\n' % string.join(self.authors, ', ')
    81 		output += 'type: %s\n' % self.type
    82 		output += 'depends: %s\n' % string.join(self.depends, ' ')
    83 		output += 'provides: %s\n' % self.provides
    84 		output += 'directory: %s\n' % self.directory
    85 		output += 'summary: %s\n' % self.summary
    86 		output += 'description: %s\n' % self.description
    87 
    88 		if self.notes:
    89 			output += 'notes: %s\n' % self.notes
    90 
    91 		return output
    92 
    93 class PluginPack:
    94 	commands = {}
    95 	plugins = {}
    96 
    97 	def load_plugins(self, types, depends):
    98 		if len(types) == 0:
    99 			types = None
   100 
   101 		if len(depends) == 0:
   102 			depends = None
   103 
   104 		for file in glob.glob('*/plugins.cfg'):
   105 			parser = ConfigParser.ConfigParser()
   106 
   107 			try:
   108 				parser.read(file)
   109 			except ConfigParser.ParsingError,  msg:
   110 				printerr('Failed to parse \'%s\':\n%s' % (file, msg))
   111 				continue
   112 
   113 			for plugin in parser.sections():
   114 				p = Plugin(os.path.dirname(file), plugin, parser)
   115 
   116 				# this is kind of hacky, but if we have types, we check to see
   117 				# if the type is in list of types to load.
   118 				if types and not p.type in types:
   119 					continue
   120 				
   121 				# now we check if the give plugins depends match the search
   122 				# depends
   123 				if depends:
   124 					if len(set(depends).intersection(set(p.depends))) == 0:
   125 						continue
   126 
   127 				self.plugins[p.provides] = p
   128 
   129 	def list_type(self, type):
   130 		list = []
   131 
   132 		for name in self.plugins.keys():
   133 			plugin = self.plugins[name]
   134 			if plugin.type == type:
   135 				list.append(plugin)
   136 
   137 		list.sort()
   138 
   139 		return list
   140 
   141 	def list_dep(self, dep):
   142 		list = []
   143 
   144 		for name in self.plugins.keys():
   145 			plugin = self.plugins[name]
   146 
   147 			if dep in plugin.depends:
   148 				list.append(plugin)
   149 
   150 		list.sort()
   151 
   152 		return list
   153 
   154 	def print_names(self, list):
   155 		names = []
   156 
   157 		for plugin in list:
   158 			names.append(plugin.name)
   159 
   160 		print string.join(names, ',')
   161 
   162 	def default_plugins(self):
   163 		return self.list_type('default')
   164 
   165 	def abusive_plugins(self):
   166 		return self.list_type('abusive')
   167 
   168 	def incomplete_plugins(self):
   169 		return self.list_type('incomplete')
   170 
   171 	def purple_plugins(self):
   172 		return self.list_dep('purple')
   173 
   174 	def finch_plugins(self):
   175 		return self.list_dep('finch')
   176 
   177 	def pidgin_plugins(self):
   178 		return self.list_dep('pidgin')
   179 
   180 	def unique_dirs(self):
   181 		dirs = {}
   182 		for name in self.plugins.keys():
   183 			dirs[self.plugins[name].directory] = 1
   184 
   185 		dirs = dirs.keys()
   186 		dirs.sort()
   187 
   188 		return dirs
   189 
   190 	def help(self, args):
   191 		"""Displays information about other commands"""
   192 		try:
   193 			cmd = self.commands[args[0]]
   194 			print cmd.__doc__
   195 		except KeyError:
   196 			print 'command \'%s\' was not found' % args[0]
   197 		except IndexError:
   198 			print '%s' % (self.help.__doc__)
   199 			print
   200 			print 'help usage:'
   201 			print '  help <command>'
   202 			print
   203 			print 'Available commands:'
   204 
   205 			cmds = self.commands.keys()
   206 			cmds.remove('help')
   207 			cmds.sort()
   208 			print '  %s' % (string.join(cmds, ' '))
   209 	commands['help'] = help
   210 
   211 	def dist_dirs(self, args):
   212 		"""Displays a list of all plugin directories to included in the distribution"""
   213 		print string.join(self.unique_dirs(), ' ')
   214 	commands['dist_dirs'] = dist_dirs
   215 
   216 	def build_dirs(self, args):
   217 		"""Displays a list of the plugins that can be built"""
   218 		if len(args) != 2:
   219 			printerr('build_dirs expects 2 arguments:')
   220 			printerr('\ta comma separated list of dependencies')
   221 			printerr('\ta comma separated list of plugins to build')
   222 			sys.exit(1)
   223 
   224 		# store the external depedencies
   225 		externals = args[0].split(',')
   226 
   227 		deps = {}
   228 
   229 		# run through the provided dependencies, setting their dependencies to
   230 		# nothing since we know we already have them
   231 		for d in externals:
   232 			deps[d] = []
   233 
   234 		# now run through the plugins adding their deps to the dictionary
   235 		for name in self.plugins.keys():
   236 			plugin = self.plugins[name]
   237 
   238 			deps[plugin.provides] = plugin.depends
   239 
   240 		# run through the requested plugins and store their plugin instance in check
   241 		check = []
   242 		for provides in args[1].split(','):
   243 			try:
   244 				if provides == 'all':
   245 					defaults = []
   246 					for p in self.default_plugins():
   247 						defaults.append(p.provides)
   248 
   249 					check += defaults
   250 
   251 					continue
   252 
   253 				plugin = self.plugins[provides]
   254 				check.append(plugin.provides)
   255 			except KeyError:
   256 				continue
   257 
   258 		# convert our list of plugins to check into a set to remove dupes
   259 		#check = set(check)
   260 
   261 		# create our list of plugins to build
   262 		build = []
   263 
   264 		# now define a function to check our deps
   265 		def has_deps(provides):
   266 			# don't add anything to build more than once
   267 			if provides in build:
   268 				return True
   269 
   270 			try:
   271 				dep_list = deps[provides]
   272 			except KeyError:
   273 				return False
   274 
   275 			# now check the dependencies
   276 			for dep in dep_list:
   277 				if '|' in dep:
   278 					count = 0
   279 					for d in dep.split('|'):
   280 						if has_deps(d):
   281 							count += 1
   282 
   283 					if count == 0:
   284 						return False
   285 				else:
   286 					if not has_deps(dep):
   287 						return False
   288 
   289 			# make sure the provides isn't an external
   290 			if not provides in externals:
   291 				build.append(provides)
   292 
   293 			# everything checks out!
   294 			return True
   295 
   296 		# check all the plugins we were told to for their dependencies
   297 		for c in check:
   298 			has_deps(c)
   299 
   300 		# now create a list of all directories to build
   301 		output = []
   302 
   303 		for provides in build:
   304 			plugin = self.plugins[provides]
   305 
   306 			output.append(plugin.directory)
   307 
   308 		output.sort()
   309 
   310 		print "%s" % (string.join(output, ','))
   311 	commands['build_dirs'] = build_dirs
   312 	
   313 	def list_plugins(self, args):
   314 		"""Displays a list similiar to 'dpkg -l' about the plugin pack"""
   315 
   316 		data = {}
   317 
   318 		# create an array for the widths, we initialize it to the lengths of
   319 		# the title strings.  We ignore summary, since that one shouldn't
   320 		# matter.
   321 		widths = [4, 8, 0]
   322 
   323 		for p in self.plugins.keys():
   324 			plugin = self.plugins[p]
   325 
   326 			if plugin.type == 'abusive':
   327 				type = 'a'
   328 			elif plugin.type == 'incomplete':
   329 				type = 'i'
   330 			else:
   331 				type = 'd'
   332 
   333 			if 'finch' in plugin.depends:
   334 				ui = 'f'
   335 			elif 'pidgin' in plugin.depends:
   336 				ui = 'p'
   337 			elif 'purple' in plugin.depends:
   338 				ui = 'r'
   339 			else:
   340 				ui = 'u'
   341 
   342 			widths[0] = max(widths[0], len(plugin.name))
   343 			widths[1] = max(widths[1], len(plugin.provides))
   344 			widths[2] = max(widths[2], len(plugin.summary))
   345 
   346 			data[plugin.provides] = [type, ui, plugin.name, plugin.provides, plugin.summary]
   347 
   348 		print 'Type=Default/Incomplete/Abusive'
   349 		print '| UI=Finch/Pidgin/puRple/Unknown'
   350 		print '|/ Name%s Provides%s Summary' % (' ' * (widths[0] - 4), ' ' * (widths[1] - 8))
   351 		print '++-%s-%s-%s' % ('=' * (widths[0]), '=' * (widths[1]), '=' * (widths[2]))
   352 
   353 		# create the format var
   354 		fmt = '%%s%%s %%-%ds %%-%ds %%s' % (widths[0], widths[1]) #, widths[2])
   355 
   356 		# now loop through the list again, with everything formatted
   357 		list = data.keys()
   358 		list.sort()
   359 
   360 		for p in list:
   361 			d = data[p]
   362 			print fmt % (d[0], d[1], d[2], d[3], d[4])
   363 	commands['list'] = list_plugins
   364 
   365 	def config_file(self, args):
   366 		"""Outputs the contents for the file to be m4_include()'d from configure"""
   367 		uniqdirs = self.unique_dirs()
   368 
   369 		# add our --with-plugins option
   370 		print 'AC_ARG_WITH(plugins,'
   371 		print '            AC_HELP_STRING([--with-plugins], [what plugins to build]),'
   372 		print '            ,with_plugins=all)'
   373 		print 'if test -z $with_plugins ; then'
   374 		print '\twith_plugins=all'
   375 		print 'fi'
   376 
   377 		# determine and add our output files
   378 		print 'PP_DIST_DIRS="%s"' % (string.join(uniqdirs, ' '))
   379 		print 'AC_SUBST(PP_DIST_DIRS)'
   380 		print
   381 		print 'AC_CONFIG_FILES(['
   382 		for dir in uniqdirs:
   383 			print '\t%s/Makefile' % (dir)
   384 		print '])'
   385 		print
   386 
   387 		# setup a second call to determine the plugins to be built
   388 		print 'PP_BUILD=`$PYTHON $srcdir/plugin_pack.py build_dirs $DEPENDENCIES $with_plugins`'
   389 		print
   390 		print 'PP_BUILD_DIRS=`echo $PP_BUILD | sed \'s/,/\ /g\'`'
   391 		print 'AC_SUBST(PP_BUILD_DIRS)'
   392 		print
   393 		print 'PP_PURPLE_BUILD="$PYTHON $srcdir/plugin_pack.py -p show_names $PP_BUILD"'
   394 		print 'PP_PIDGIN_BUILD="$PYTHON $srcdir/plugin_pack.py -P show_names $PP_BUILD"'
   395 		print 'PP_FINCH_BUILD="$PYTHON $srcdir/plugin_pack.py -f show_names $PP_BUILD"'
   396 	commands['config_file'] = config_file
   397 
   398 	def dependency_graph(self, args):
   399 		"""Outputs a graphviz script showing plugin dependencies"""
   400 		def node_label(plugin):
   401 			node = plugin.provides.replace('-', '_')
   402 			label = plugin.name
   403 
   404 			return node, label
   405 
   406 		def print_plugins(list):
   407 			for plugin in list:
   408 				node, label = node_label(plugin)
   409 
   410 				print '\t%s[label="%s"];' % (node, label)
   411 
   412 		print 'digraph {'
   413 		print '\tlabel="Dependency Graph";'
   414 		print '\tlabelloc="t";'
   415 		print '\tsplines=TRUE;'
   416 		print '\toverlap=FALSE;'
   417 		print
   418 		print '\tnode[fontname="sans", fontsize="8", style="filled"];'
   419 		print
   420 
   421 		# run through the default plugins
   422 		print '\t/* default plugins */'
   423 		print '\tnode[fillcolor="palegreen",shape="tab"];'
   424 		print_plugins(self.default_plugins())
   425 		print
   426 
   427 		# run through the incomplete plugins
   428 		print '\t/* incomplete plugins */'
   429 		print '\tnode[fillcolor="lightyellow1",shape="note"];'
   430 		print_plugins(self.incomplete_plugins())
   431 		print
   432 
   433 		# run through the abusive plugins
   434 		print '\t/* abusive plugins */'
   435 		print '\tnode[fillcolor="lightpink",shape="octagon"];'
   436 		print_plugins(self.abusive_plugins())
   437 		print
   438 
   439 		# run through again, this time showing the relations
   440 		print '\t/* dependencies'
   441 		print '\t * exteranl ones that don\'t have nodes get colored to the following'
   442 		print '\t */'
   443 		print '\tnode[fillcolor="powderblue", shape="egg"];'
   444 
   445 		for name in self.plugins.keys():
   446 			plugin = self.plugins[name]
   447 
   448 			node, label = node_label(plugin)
   449 
   450 			for dep in plugin.depends:
   451 				dep = dep.replace('-', '_')
   452 				print '\t%s -> %s;' % (node, dep)
   453 
   454 		print '}'
   455 	commands['dependency_graph'] = dependency_graph
   456 
   457 	def debian_description(self, args):
   458 		"""Outputs the description for the Debian packages"""
   459 		print 'Description: %d useful plugins for Pidgin, Finch, and Purple' % len(self.plugins)
   460 		print ' The Plugin Pack is a collection of many simple-yet-useful plugins for Pidgin,'
   461 		print ' Finch, and Purple.  You will find a summary of each plugin below.  For more'
   462 		print ' about an individual plugin, please see %s' % webpage
   463 		print ' .'
   464 		print ' Note: not all %d of these plugins are currently usable' % len(self.plugins)
   465 		
   466 		list = self.plugins.keys()
   467 		list.sort()
   468 		for key in list:
   469 			plugin = self.plugins[key]
   470 			print ' .'
   471 			print ' %s: %s' % (plugin.name, plugin.summary)
   472 
   473 		print ' .'
   474 		print ' .'
   475 		print ' Homepage: %s' % webpage
   476 	commands['debian_description'] = debian_description
   477 
   478 	def show_names(self, args):
   479 		"""Displays the names of the given comma separated list of provides"""
   480 
   481 		if len(args) == 0 or len(args[0]) == 0:
   482 			printerr('show_names expects a comma separated list of provides')
   483 			sys.exit(1)
   484 
   485 		provides = args[0].split(',')
   486 		if len(provides) == 0:
   487 			print "none"
   488 
   489 		line = " "
   490 
   491 		for provide in provides:
   492 			if not provide in self.plugins:
   493 				continue
   494 
   495 			name = self.plugins[provide].name
   496 
   497 			if len(line) + len(name) + 2 > 75:
   498 				print line.rstrip(',')
   499 				line = ' '
   500 			
   501 			line += ' %s,' % name 
   502 
   503 		if len(line) > 1:
   504 			print line.rstrip(',')
   505 	commands['show_names'] = show_names
   506 
   507 	def info(self, args):
   508 		"""Displays all information about the given plugins"""
   509 		for p in args:
   510 			try:
   511 				print self.plugins[p].__str__().strip()
   512 			except KeyError:
   513 				print 'Failed to find a plugin that provides \'%s\'' % (p)
   514 
   515 			print
   516 	commands['info'] = info
   517 
   518 	def stats(self, args):
   519 		"""Displays stats about the plugin pack"""
   520 		counts = {}
   521 		
   522 		counts['total'] = len(self.plugins)
   523 		counts['default'] = len(self.default_plugins())
   524 		counts['incomplete'] = len(self.incomplete_plugins())
   525 		counts['abusive'] = len(self.abusive_plugins())
   526 		counts['purple'] = len(self.purple_plugins())
   527 		counts['finch'] = len(self.finch_plugins())
   528 		counts['pidgin'] = len(self.pidgin_plugins())
   529 
   530 		def value(val):
   531 			return "%3d (%6.2f%%)" % (val, (float(val) / float(counts['total'])) * 100.0)
   532 
   533 		print "Purple Plugin Pack Stats"
   534 		print ""
   535 		print "%d plugins in total" % (counts['total'])
   536 		print
   537 		print "Status:"
   538 		print "  complete:   %s" % (value(counts['default']))
   539 		print "  incomplete: %s" % (value(counts['incomplete']))
   540 		print "  abusive:    %s" % (value(counts['abusive']))
   541 		print ""
   542 		print "Type:"
   543 		print "  purple: %s" % (value(counts['purple']))
   544 		print "  finch:  %s" % (value(counts['finch']))
   545 		print "  pidgin: %s" % (value(counts['pidgin']))
   546 	commands['stats'] = stats
   547 
   548 def show_usage(pp, exitcode):
   549 	print __doc__
   550 
   551 	cmds = pp.commands.keys()
   552 	cmds.sort()
   553 	
   554 	for cmd in cmds:
   555 		print "  %-20s %s" % (cmd, pp.commands[cmd].__doc__)
   556 
   557 	print ""
   558 
   559 	sys.exit(exitcode)
   560 
   561 def main():
   562 	# create our main instance
   563 	pp = PluginPack()
   564 
   565 	types = []
   566 	depends = []
   567 
   568 	try:
   569 		shortopts = 'adfiPp'
   570 
   571 		opts, args = getopt.getopt(sys.argv[1:], shortopts)
   572 	except getopt.error, msg:
   573 		print msg
   574 		show_usage(pp, 1)
   575 
   576 	for o, a in opts:
   577 		if o == '-a':
   578 			types.append('abusive')
   579 		elif o == '-d':
   580 			types.append('default')
   581 		elif o == '-i':
   582 			types.append('incomplete')
   583 		elif o == '-f':
   584 			depends.append('finch')
   585 		elif o == '-P':
   586 			depends.append('pidgin')
   587 		elif o == '-p':
   588 			depends.append('purple')
   589 
   590 	# load the plugins that have been requested, if both lists are empty, all
   591 	# plugins are loaded
   592 	pp.load_plugins(types, depends)
   593 
   594 	if(len(args) == 0):
   595 		show_usage(pp, 1)
   596 
   597 	cmd = args[0]
   598 	args = args[1:]
   599 
   600 	try:
   601 		pp.commands[cmd](pp, args)
   602 	except KeyError:
   603 		printerr('\'%s\' command not found' % (cmd))
   604 
   605 if __name__ == '__main__':
   606 	# this is a work around when we're called for a directory that isn't the
   607 	# directory that this file is in.  This happens during distcheck, as well
   608 	# as a few other cases that most people won't use ;)
   609 	if os.path.dirname(__file__) != '':
   610 		os.chdir(os.path.dirname(__file__))
   611 
   612 	main()
   613