changeset 0:7301d534bc6c

initial messy and incomplete strawman prototype for Mozilla (Firefox) profile management
author Jeff Hammel <k0scist@gmail.com>
date Sun, 04 Apr 2010 18:49:55 -0400
parents
children 979315ed0816
files profilemanager/__init__.py profilemanager/command.py profilemanager/config.py profilemanager/main.py profilemanager/manager.py setup.py
diffstat 6 files changed, 290 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /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 @@
+#
--- /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
+
--- /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
+    
--- /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 <options> command <command-options>'
+    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()
--- /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"""
+
--- /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
+      """,
+      )