# HG changeset patch # User Jeff Hammel # Date 1270421395 14400 # Node ID 7301d534bc6ca5081db6014e314ec9ae5c83c4fd initial messy and incomplete strawman prototype for Mozilla (Firefox) profile management diff -r 000000000000 -r 7301d534bc6c profilemanager/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/profilemanager/__init__.py Sun Apr 04 18:49:55 2010 -0400 @@ -0,0 +1,1 @@ +# diff -r 000000000000 -r 7301d534bc6c profilemanager/command.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/profilemanager/command.py Sun Apr 04 18:49:55 2010 -0400 @@ -0,0 +1,98 @@ +""" +a command-line interface to the command line, a la pythonpaste +""" + +import inspect +import sys +from optparse import OptionParser + +if 'commands' not in globals(): + commands = {} + +def command(function): + # XXX should get bound/unbound state from function (how?) + global commands + name = function.func_name + doc = inspect.cleandoc(function.__doc__) + argspec = inspect.getargspec(function) + defaults = argspec.defaults + if defaults: + args = argspec.args[1:-len(defaults)] + optional = dict(zip(argspec.args[-len(defaults):], defaults)) + else: + args = argspec.args[1:] + optional = None + commands[name] = { 'doc': doc, + 'args': args, + 'optional': optional, + 'varargs': argspec.varargs + } + return function + +def commandargs2str(command): + if isinstance(command, basestring): + command = commands[command] + retval = [] + retval.extend(['<%s>' % arg for arg in command['args']]) + varargs = command['varargs'] + if varargs: + retval.append('<%s> [%s] [...]' % (varargs, varargs)) + if command['optional']: + retval.append('[options]') + return ' '.join(retval) + +def list_commands(): + for command in sorted(commands.keys()): + print '%s %s' % (command, commandargs2str(command)) + print '\n%s\n' % commands[command]['doc'] + +def doc2arghelp(docstring, decoration='-', delimeter=':'): + """ + Parse a docstring and get at the section describing arguments + - decoration: decoration character + - delimeter: delimter character + + Yields a tuple of the stripped docstring and the arguments help + dictionary + """ + lines = [ i.strip() for i in docstring.split('\n') ] + argdict = {} + doc = [] + option = None + for line in lines: + if not line and option: # blank lines terminate + break + if line.startswith(decoration) and delimeter in line: + name, description = line.split(delimeter, 1) + name = name.lstrip(decoration).strip() + description = description.strip() + argdict[name] = [ description ] + option = name + else: + if option: + argdict[name].append(line) + else: + doc.append(line) + argdict = dict([(key, ' '.join(value)) + for key, value in argdict.items()]) + return ('\n'.join(doc), argdict) + +def command2parser(command): + doc, argdict = doc2arghelp(commands[command]['doc']) + parser = OptionParser('%%prog %s %s' % (command, commandargs2str(command)), + description=doc, add_help_option=False) + if commands[command]['optional']: + for key, value in commands[command]['optional'].items(): + help = argdict.get(key) + if value is True: + parser.add_option('--no-%s' % key, dest=key, + action='store_false', default=True, + help=help) + elif value is False: + parser.add_option('--%s' % key, action='store_true', + default=False, help=help) + else: + parser.add_option('--%s' % key, help=help) + + return parser + diff -r 000000000000 -r 7301d534bc6c profilemanager/config.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/profilemanager/config.py Sun Apr 04 18:49:55 2010 -0400 @@ -0,0 +1,15 @@ +""" +objects + methods related to .ini objects +""" + +# XXX do this inline as opposed to using e.g. martINI for portability +# and ease of modification + +def dictionary(parser): + """ + obtain a nested dictionary from an .ini file + """ + +if __name__ == '__main__': + from pprint import pprint + diff -r 000000000000 -r 7301d534bc6c profilemanager/main.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/profilemanager/main.py Sun Apr 04 18:49:55 2010 -0400 @@ -0,0 +1,76 @@ +#!/usr/bin/env python + +import os +import sys + +from manager import ProfileManager +from optparse import OptionGroup +from optparse import OptionParser +from command import commands, commandargs2str, command2parser + +# could go in commands +def print_help(parser): + parser.print_help() + # short descriptions for commands + command_descriptions = [dict(name=i, + description = commands[i]['doc'].strip().split('\n',1)[0]) + for i in sorted(commands.keys())] + max_len = max([len(i['name']) for i in command_descriptions]) + description = "Commands: \n%s" % ('\n'.join([' %s%s %s' % (description['name'], ' ' * (max_len - len(description['name'])), description['description']) + for description in command_descriptions])) + + print + print description + +def main(args=sys.argv[1:]): + + # global option parsing + usage = '%prog command ' + parser = OptionParser(usage, description='run `%prog help` to display commands') + parser.add_option('-c', '--config', dest='config', + help="specify a profile.ini [default: $HOME/.mozilla/firefox/profiles.ini]") + parser.disable_interspersed_args() + options, args = parser.parse_args(args) + + # help/sanity check -- should probably be separated + if not len(args): + print_help(parser) + sys.exit(0) + if args[0] == 'help': + if len(args) == 2: + if args[1] in commands: + name = args[1] + commandparser = command2parser(name) + commandparser.print_help() + else: + parser.error("No command '%s'" % args[1]) + else: + print_help(parser) + sys.exit(0) + command = args[0] + if command not in commands: + parser.error("No command '%s'" % command) + + # XXX to move it its own method -- this is the only program-specific code + if options.config is None: + # XXX unix-specific + options.config = os.path.join(os.environ['HOME'], '.mozilla/firefox/profiles.ini') + if not os.path.exists(options.config): + parser.error('%s does not exist' % options.config) + manager = ProfileManager(options.config) + + # command specific args + name = args[0] + command = commands[name] + commandparser = command2parser(name) + command_options, command_args = commandparser.parse_args(args[1:]) + if len(command_args) < len(command['args']): + commandparser.error("Not enough arguments given") + if len(command_args) != len(command['args']) and not command['varargs']: + commandparser.error("Too many arguments given") + + # invoke the command + getattr(manager, name)(*command_args, **command_options.__dict__) + +if __name__ == '__main__': + main() diff -r 000000000000 -r 7301d534bc6c profilemanager/manager.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/profilemanager/manager.py Sun Apr 04 18:49:55 2010 -0400 @@ -0,0 +1,72 @@ +""" +manage Mozilla/Firefox profiles +""" + +import os +import shutil + +from command import command + +class ProfileNotFound(Exception): + """ + exception when a profile is specified but is not present in a given + .ini file + """ + +class ProfileManager(object): + + def __init__(self, profiles): + """ + - profiles: profiles.ini file + """ + self.profiles = profiles + self.directory = '' # TODO : path to self.profiles directory + + ### (public) API + + @command + def clone(self, source, dest): + """ + clones the profile `source` and output to `dest` + """ + source_path = self.path(source) # fs path of the `from` profile + dest_path = self.path(dest) # fs path to back up to + shutil.copytree(src_path, backup, symlinks=False) + + @command + def backup(self, profile, dest=None): + """ + backup the profile + - profile: name of the profile to be backed up + - dest: name of the destination (optional) + """ + # XXX should use `self.clone` ! + if dest is None: + dest = '' + self.clone(profile, dest) + # TODO: add something like + # `Backup=$(profile)s.$(datestamp)s.bak` + # to self.profiles + + @command + def restore(self, profile, date=None, delete=False): + """ + restore the profile from a backup + the most recent backup is used unless `date` is given + - date : date to restore from + - delete : delete the backup after restoration + """ + if delete: + # delete the backup + pass + + @command + def merge(self, *profiles): + """merge a set of profiles (not trivial!)""" + raise NotImplementedError + + ### internal functions + + def path(self, profile): + """returns the path to the profile""" + diff -r 000000000000 -r 7301d534bc6c setup.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/setup.py Sun Apr 04 18:49:55 2010 -0400 @@ -0,0 +1,28 @@ +from setuptools import setup, find_packages +import sys, os + +version = '0.0' + +setup(name='ProfileManager', + version=version, + description='profile manager for Firefox and other Mozilla products', + long_description="""\ +""", + classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers + keywords='', + author='Jeff Hammel', + author_email='jhammel@mozilla.com', + url='', + license='', + packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), + include_package_data=True, + zip_safe=False, + install_requires=[ + # -*- Extra requirements: -*- + ], + entry_points=""" + # -*- Entry points: -*- + [console_scripts] + ProfileManager = profilemanager.main:main + """, + )