Code

- put all methods for parsing a message into a list and call all in a
[roundup.git] / roundup / admin.py
index 51695e2bd46b44789f4bb88ba23532d631070ab5..1a4e241df277116f00424244eefa5658264f3a4c 100644 (file)
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 #
-# $Id: admin.py,v 1.110 2008-02-07 03:28:33 richard Exp $
 
-'''Administration commands for maintaining Roundup trackers.
-'''
+"""Administration commands for maintaining Roundup trackers.
+"""
 __docformat__ = 'restructuredtext'
 
-import csv, getopt, getpass, os, re, shutil, sys, UserDict
+import csv, getopt, getpass, os, re, shutil, sys, UserDict, operator
 
 from roundup import date, hyperdb, roundupdb, init, password, token
 from roundup import __version__ as roundup_version
 import roundup.instance
 from roundup.configuration import CoreConfig
 from roundup.i18n import _
+from roundup.exceptions import UsageError
 
 class CommandDict(UserDict.UserDict):
-    '''Simple dictionary that lets us do lookups using partial keys.
+    """Simple dictionary that lets us do lookups using partial keys.
 
     Original code submitted by Engelbert Gruber.
-    '''
+    """
     _marker = []
     def get(self, key, default=_marker):
-        if self.data.has_key(key):
+        if key in self.data:
             return [(key, self.data[key])]
-        keylist = self.data.keys()
-        keylist.sort()
+        keylist = sorted(self.data)
         l = []
         for ki in keylist:
             if ki.startswith(key):
                 l.append((ki, self.data[ki]))
         if not l and default is self._marker:
-            raise KeyError, key
+            raise KeyError(key)
         return l
 
-class UsageError(ValueError):
-    pass
-
 class AdminTool:
-    ''' A collection of methods used in maintaining Roundup trackers.
+    """ A collection of methods used in maintaining Roundup trackers.
 
         Typically these methods are accessed through the roundup-admin
         script. The main() method provided on this class gives the main
@@ -63,14 +59,14 @@ class AdminTool:
         given in the method docstring.
 
         Additional help may be supplied by help_*() methods.
-    '''
+    """
     def __init__(self):
         self.commands = CommandDict()
-        for k in AdminTool.__dict__.keys():
+        for k in AdminTool.__dict__:
             if k[:3] == 'do_':
                 self.commands[k[3:]] = getattr(self, k)
         self.help = {}
-        for k in AdminTool.__dict__.keys():
+        for k in AdminTool.__dict__:
             if k[:5] == 'help_':
                 self.help[k[5:]] = getattr(self, k)
         self.tracker_home = ''
@@ -78,27 +74,27 @@ class AdminTool:
         self.db_uncommitted = False
 
     def get_class(self, classname):
-        '''Get the class - raise an exception if it doesn't exist.
-        '''
+        """Get the class - raise an exception if it doesn't exist.
+        """
         try:
             return self.db.getclass(classname)
         except KeyError:
-            raise UsageError, _('no such class "%(classname)s"')%locals()
+            raise UsageError(_('no such class "%(classname)s"')%locals())
 
     def props_from_args(self, args):
-        ''' Produce a dictionary of prop: value from the args list.
+        """ Produce a dictionary of prop: value from the args list.
 
             The args list is specified as ``prop=value prop=value ...``.
-        '''
+        """
         props = {}
         for arg in args:
             if arg.find('=') == -1:
-                raise UsageError_('argument "%(arg)s" not propname=value'
-                    )%locals()
+                raise UsageError(_('argument "%(arg)s" not propname=value'
+                    )%locals())
             l = arg.split('=')
             if len(l) < 2:
-                raise UsageError_('argument "%(arg)s" not propname=value'
-                    )%locals()
+                raise UsageError(_('argument "%(arg)s" not propname=value'
+                    )%locals())
             key, value = l[0], '='.join(l[1:])
             if value:
                 props[key] = value
@@ -107,11 +103,11 @@ class AdminTool:
         return props
 
     def usage(self, message=''):
-        ''' Display a simple usage message.
-        '''
+        """ Display a simple usage message.
+        """
         if message:
             message = _('Problem: %(message)s\n\n')%locals()
-        print _('''%(message)sUsage: roundup-admin [options] [<command> <arguments>]
+        print _("""%(message)sUsage: roundup-admin [options] [<command> <arguments>]
 
 Options:
  -i instance home  -- specify the issue tracker "home directory" to administer
@@ -132,15 +128,15 @@ Help:
  roundup-admin help                       -- this help
  roundup-admin help <command>             -- command-specific help
  roundup-admin help all                   -- all available help
-''')%locals()
+""")%locals()
         self.help_commands()
 
     def help_commands(self):
-        ''' List the commands available with their help summary.
-        '''
+        """List the commands available with their help summary.
+        """
         print _('Commands:'),
         commands = ['']
-        for command in self.commands.values():
+        for command in self.commands.itervalues():
             h = _(command.__doc__).split('\n')[0]
             commands.append(' '+h[7:])
         commands.sort()
@@ -151,20 +147,18 @@ matches only one command, e.g. l == li == lis == list."""))
         print
 
     def help_commands_html(self, indent_re=re.compile(r'^(\s+)\S+')):
-        ''' Produce an HTML command list.
-        '''
-        commands = self.commands.values()
-        def sortfun(a, b):
-            return cmp(a.__name__, b.__name__)
-        commands.sort(sortfun)
+        """ Produce an HTML command list.
+        """
+        commands = sorted(self.commands.itervalues(),
+            operator.attrgetter('__name__'))
         for command in commands:
             h = _(command.__doc__).split('\n')
             name = command.__name__[3:]
             usage = h[0]
-            print '''
+            print """
 <tr><td valign=top><strong>%(name)s</strong></td>
     <td><tt>%(usage)s</tt><p>
-<pre>''' % locals()
+<pre>""" % locals()
             indent = indent_re.match(h[3])
             if indent: indent = len(indent.group(1))
             for line in h[3:]:
@@ -175,7 +169,7 @@ matches only one command, e.g. l == li == lis == list."""))
             print '</pre></td></tr>\n'
 
     def help_all(self):
-        print _('''
+        print _("""
 All commands (except help) require a tracker specifier. This is just
 the path to the roundup tracker you're working with. A roundup tracker
 is where roundup keeps the database and configuration file that defines
@@ -236,21 +230,21 @@ Date format examples:
   "." means "right now"
 
 Command help:
-''')
+""")
         for name, command in self.commands.items():
             print _('%s:')%name
             print '   ', _(command.__doc__)
 
     def do_help(self, args, nl_re=re.compile('[\r\n]'),
             indent_re=re.compile(r'^(\s+)\S+')):
-        ""'''Usage: help topic
+        ''"""Usage: help topic
         Give help about topic.
 
         commands  -- list commands
         <command> -- help specific to a command
         initopts  -- init command options
         all       -- all available help
-        '''
+        """
         if len(args)>0:
             topic = args[0]
         else:
@@ -258,7 +252,7 @@ Command help:
 
 
         # try help_ methods
-        if self.help.has_key(topic):
+        if topic in self.help:
             self.help[topic]()
             return 0
 
@@ -283,7 +277,7 @@ Command help:
         return 0
 
     def listTemplates(self):
-        ''' List all the available templates.
+        """ List all the available templates.
 
         Look in the following places, where the later rules take precedence:
 
@@ -299,7 +293,7 @@ Command help:
             this is for when someone unpacks a 3rd-party template
          5. <current working dir>
             this is for someone who "cd"s to the 3rd-party template dir
-        '''
+        """
         # OK, try <prefix>/share/roundup/templates
         #     and <egg-directory>/share/roundup/templates
         # -- this module (roundup.admin) will be installed in something
@@ -343,13 +337,13 @@ Command help:
 
     def help_initopts(self):
         templates = self.listTemplates()
-        print _('Templates:'), ', '.join(templates.keys())
+        print _('Templates:'), ', '.join(templates)
         import roundup.backends
         backends = roundup.backends.list_backends()
         print _('Back ends:'), ', '.join(backends)
 
     def do_install(self, tracker_home, args):
-        ""'''Usage: install [template [backend [key=val[,key=val]]]]
+        ''"""Usage: install [template [backend [key=val[,key=val]]]]
         Install a new Roundup tracker.
 
         The command will prompt for the tracker home directory
@@ -370,21 +364,21 @@ Command help:
         the tracker's dbinit.py module init() function.
 
         See also initopts help.
-        '''
+        """
         if len(args) < 1:
-            raise UsageError, _('Not enough arguments supplied')
+            raise UsageError(_('Not enough arguments supplied'))
 
         # make sure the tracker home can be created
         tracker_home = os.path.abspath(tracker_home)
         parent = os.path.split(tracker_home)[0]
         if not os.path.exists(parent):
-            raise UsageError_('Instance home parent directory "%(parent)s"'
-                ' does not exist')%locals()
+            raise UsageError(_('Instance home parent directory "%(parent)s"'
+                ' does not exist')%locals())
 
         config_ini_file = os.path.join(tracker_home, CoreConfig.INI_FILE)
         # check for both old- and new-style configs
-        if filter(os.path.exists, [config_ini_file,
-                os.path.join(tracker_home, 'config.py')]):
+        if list(filter(os.path.exists, [config_ini_file,
+                os.path.join(tracker_home, 'config.py')])):
             ok = raw_input(_(
 """WARNING: There appears to be a tracker in "%(tracker_home)s"!
 If you re-install it, you will lose all the data!
@@ -398,9 +392,9 @@ Erase it? Y/N: """) % locals())
         # select template
         templates = self.listTemplates()
         template = len(args) > 1 and args[1] or ''
-        if not templates.has_key(template):
-            print _('Templates:'), ', '.join(templates.keys())
-        while not templates.has_key(template):
+        if template not in templates:
+            print _('Templates:'), ', '.join(templates)
+        while template not in templates:
             template = raw_input(_('Select template [classic]: ')).strip()
             if not template:
                 template = 'classic'
@@ -442,8 +436,8 @@ Erase it? Y/N: """) % locals())
         need_set = CoreConfig(tracker_home)._get_unset_options()
         if need_set:
             print _(" ... at a minimum, you must set following options:")
-            for section, options in need_set.items():
-                print "   [%s]: %s" % (section, ", ".join(options))
+            for section in need_set:
+                print "   [%s]: %s" % (section, ", ".join(need_set[section]))
 
         # note about schema modifications
         print _("""
@@ -464,23 +458,23 @@ Erase it? Y/N: """) % locals())
         return 0
 
     def do_genconfig(self, args):
-        ""'''Usage: genconfig <filename>
+        ''"""Usage: genconfig <filename>
         Generate a new tracker config file (ini style) with default values
         in <filename>.
-        '''
+        """
         if len(args) < 1:
-            raise UsageError, _('Not enough arguments supplied')
+            raise UsageError(_('Not enough arguments supplied'))
         config = CoreConfig()
         config.save(args[0])
 
     def do_initialise(self, tracker_home, args):
-        ""'''Usage: initialise [adminpw]
+        ''"""Usage: initialise [adminpw]
         Initialise a new Roundup tracker.
 
         The administrator details will be set at this step.
 
         Execute the tracker's initialisation function dbinit.init()
-        '''
+        """
         # password
         if len(args) > 1:
             adminpw = args[1]
@@ -493,11 +487,11 @@ Erase it? Y/N: """) % locals())
 
         # make sure the tracker home is installed
         if not os.path.exists(tracker_home):
-            raise UsageError, _('Instance home does not exist')%locals()
+            raise UsageError(_('Instance home does not exist')%locals())
         try:
             tracker = roundup.instance.open(tracker_home)
         except roundup.instance.TrackerError:
-            raise UsageError, _('Instance has not been installed')%locals()
+            raise UsageError(_('Instance has not been installed')%locals())
 
         # is there already a database?
         if tracker.exists():
@@ -514,23 +508,26 @@ Erase it? Y/N: """))
             tracker.nuke()
 
             # re-write the backend select file
-            init.write_select_db(tracker_home, backend)
+            init.write_select_db(tracker_home, backend, tracker.config.DATABASE)
 
         # GO
-        tracker.init(password.Password(adminpw))
+        tracker.init(password.Password(adminpw, config=tracker.config))
 
         return 0
 
 
     def do_get(self, args):
-        ""'''Usage: get property designator[,designator]*
+        ''"""Usage: get property designator[,designator]*
         Get the given property of one or more designator(s).
 
+        A designator is a classname and a nodeid concatenated,
+        eg. bug1, user10, ...
+
         Retrieves the property value of the nodes specified
         by the designators.
-        '''
+        """
         if len(args) < 2:
-            raise UsageError, _('Not enough arguments supplied')
+            raise UsageError(_('Not enough arguments supplied'))
         propname = args[0]
         designators = args[1].split(',')
         l = []
@@ -539,7 +536,7 @@ Erase it? Y/N: """))
             try:
                 classname, nodeid = hyperdb.splitDesignator(designator)
             except hyperdb.DesignatorError, message:
-                raise UsageError, message
+                raise UsageError(message)
 
             # get the class
             cl = self.get_class(classname)
@@ -563,7 +560,9 @@ Erase it? Y/N: """))
                         property = properties[propname]
                         if not (isinstance(property, hyperdb.Multilink) or
                           isinstance(property, hyperdb.Link)):
-                            raise UsageError, _('property %s is not of type Multilink or Link so -d flag does not apply.')%propname
+                            raise UsageError(_('property %s is not of type'
+                                ' Multilink or Link so -d flag does not '
+                                'apply.')%propname)
                         propclassname = self.db.getclass(property.classname).classname
                         id = cl.get(nodeid, propname)
                         for i in id:
@@ -578,7 +577,9 @@ Erase it? Y/N: """))
                         property = properties[propname]
                         if not (isinstance(property, hyperdb.Multilink) or
                           isinstance(property, hyperdb.Link)):
-                            raise UsageError, _('property %s is not of type Multilink or Link so -d flag does not apply.')%propname
+                            raise UsageError(_('property %s is not of type'
+                                ' Multilink or Link so -d flag does not '
+                                'apply.')%propname)
                         propclassname = self.db.getclass(property.classname).classname
                         id = cl.get(nodeid, propname)
                         for i in id:
@@ -586,10 +587,11 @@ Erase it? Y/N: """))
                     else:
                         print cl.get(nodeid, propname)
             except IndexError:
-                raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
+                raise UsageError(_('no such %(classname)s node '
+                    '"%(nodeid)s"')%locals())
             except KeyError:
-                raise UsageError_('no such %(classname)s property '
-                    '"%(propname)s"')%locals()
+                raise UsageError(_('no such %(classname)s property '
+                    '"%(propname)s"')%locals())
         if self.separator:
             print self.separator.join(l)
 
@@ -597,19 +599,22 @@ Erase it? Y/N: """))
 
 
     def do_set(self, args):
-        ""'''Usage: set items property=value property=value ...
+        ''"""Usage: set items property=value property=value ...
         Set the given properties of one or more items(s).
 
         The items are specified as a class or as a comma-separated
         list of item designators (ie "designator[,designator,...]").
 
+        A designator is a classname and a nodeid concatenated,
+        eg. bug1, user10, ...
+
         This command sets the properties to the values for all designators
         given. If the value is missing (ie. "property=") then the property
         is un-set. If the property is a multilink, you specify the linked
         ids for the multilink as comma-separated numbers (ie "1,2,3").
-        '''
+        """
         if len(args) < 2:
-            raise UsageError, _('Not enough arguments supplied')
+            raise UsageError(_('Not enough arguments supplied'))
         from roundup import hyperdb
 
         designators = args[0].split(',')
@@ -625,7 +630,7 @@ Erase it? Y/N: """))
             try:
                 designators = [hyperdb.splitDesignator(x) for x in designators]
             except hyperdb.DesignatorError, message:
-                raise UsageError, message
+                raise UsageError(message)
 
         # get the props from the args
         props = self.props_from_args(args[1:])
@@ -640,27 +645,27 @@ Erase it? Y/N: """))
                     props[key] = hyperdb.rawToHyperdb(self.db, cl, itemid,
                         key, value)
                 except hyperdb.HyperdbValueError, message:
-                    raise UsageError, message
+                    raise UsageError(message)
 
             # try the set
             try:
-                apply(cl.set, (itemid, ), props)
+                cl.set(itemid, **props)
             except (TypeError, IndexError, ValueError), message:
                 import traceback; traceback.print_exc()
-                raise UsageError, message
+                raise UsageError(message)
         self.db_uncommitted = True
         return 0
 
     def do_find(self, args):
-        ""'''Usage: find classname propname=value ...
+        ''"""Usage: find classname propname=value ...
         Find the nodes of the given class with a given link property value.
 
         Find the nodes of the given class with a given link property value.
         The value may be either the nodeid of the linked node, or its key
         value.
-        '''
+        """
         if len(args) < 1:
-            raise UsageError, _('Not enough arguments supplied')
+            raise UsageError(_('Not enough arguments supplied'))
         classname = args[0]
         # get the class
         cl = self.get_class(classname)
@@ -669,7 +674,7 @@ Erase it? Y/N: """))
         props = self.props_from_args(args[1:])
 
         # convert the user-input value to a value used for find()
-        for propname, value in props.items():
+        for propname, value in props.iteritems():
             if ',' in value:
                 values = value.split(',')
             else:
@@ -689,85 +694,88 @@ Erase it? Y/N: """))
             designator = []
             if self.separator:
                 if self.print_designator:
-                    id=apply(cl.find, (), props)
+                    id = cl.find(**props)
                     for i in id:
                         designator.append(classname + i)
                     print self.separator.join(designator)
                 else:
-                    print self.separator.join(apply(cl.find, (), props))
+                    print self.separator.join(cl.find(**props))
 
             else:
                 if self.print_designator:
-                    id=apply(cl.find, (), props)
+                    id = cl.find(**props)
                     for i in id:
                         designator.append(classname + i)
                     print designator
                 else:
-                    print apply(cl.find, (), props)
+                    print cl.find(**props)
         except KeyError:
-            raise UsageError_('%(classname)s has no property '
-                '"%(propname)s"')%locals()
+            raise UsageError(_('%(classname)s has no property '
+                '"%(propname)s"')%locals())
         except (ValueError, TypeError), message:
-            raise UsageError, message
+            raise UsageError(message)
         return 0
 
     def do_specification(self, args):
-        ""'''Usage: specification classname
+        ''"""Usage: specification classname
         Show the properties for a classname.
 
         This lists the properties for a given class.
-        '''
+        """
         if len(args) < 1:
-            raise UsageError, _('Not enough arguments supplied')
+            raise UsageError(_('Not enough arguments supplied'))
         classname = args[0]
         # get the class
         cl = self.get_class(classname)
 
         # get the key property
         keyprop = cl.getkey()
-        for key, value in cl.properties.items():
+        for key in cl.properties:
+            value = cl.properties[key]
             if keyprop == key:
                 print _('%(key)s: %(value)s (key property)')%locals()
             else:
                 print _('%(key)s: %(value)s')%locals()
 
     def do_display(self, args):
-        ""'''Usage: display designator[,designator]*
+        ''"""Usage: display designator[,designator]*
         Show the property values for the given node(s).
 
+        A designator is a classname and a nodeid concatenated,
+        eg. bug1, user10, ...
+
         This lists the properties and their associated values for the given
         node.
-        '''
+        """
         if len(args) < 1:
-            raise UsageError, _('Not enough arguments supplied')
+            raise UsageError(_('Not enough arguments supplied'))
 
         # decode the node designator
         for designator in args[0].split(','):
             try:
                 classname, nodeid = hyperdb.splitDesignator(designator)
             except hyperdb.DesignatorError, message:
-                raise UsageError, message
+                raise UsageError(message)
 
             # get the class
             cl = self.get_class(classname)
 
             # display the values
-            keys = cl.properties.keys()
-            keys.sort()
+            keys = sorted(cl.properties)
             for key in keys:
                 value = cl.get(nodeid, key)
                 print _('%(key)s: %(value)s')%locals()
 
     def do_create(self, args):
-        ""'''Usage: create classname property=value ...
+        ''"""Usage: create classname property=value ...
         Create a new entry of a given class.
 
         This creates a new entry of the given class using the property
         name=value arguments provided on the command line after the "create"
         command.
-        '''
+        """
         if len(args) < 1:
-            raise UsageError, _('Not enough arguments supplied')
+            raise UsageError(_('Not enough arguments supplied'))
         from roundup import hyperdb
 
         classname = args[0]
@@ -780,8 +788,9 @@ Erase it? Y/N: """))
         properties = cl.getprops(protected = 0)
         if len(args) == 1:
             # ask for the properties
-            for key, value in properties.items():
+            for key in properties:
                 if key == 'id': continue
+                value = properties[key]
                 name = value.__class__.__name__
                 if isinstance(value , hyperdb.Password):
                     again = None
@@ -802,29 +811,29 @@ Erase it? Y/N: """))
             props = self.props_from_args(args[1:])
 
         # convert types
-        for propname, value in props.items():
+        for propname in props:
             try:
                 props[propname] = hyperdb.rawToHyperdb(self.db, cl, None,
-                    propname, value)
+                    propname, props[propname])
             except hyperdb.HyperdbValueError, message:
-                raise UsageError, message
+                raise UsageError(message)
 
         # check for the key property
         propname = cl.getkey()
-        if propname and not props.has_key(propname):
-            raise UsageError_('you must provide the "%(propname)s" '
-                'property.')%locals()
+        if propname and propname not in props:
+            raise UsageError(_('you must provide the "%(propname)s" '
+                'property.')%locals())
 
         # do the actual create
         try:
-            print apply(cl.create, (), props)
+            print cl.create(**props)
         except (TypeError, IndexError, ValueError), message:
-            raise UsageError, message
+            raise UsageError(message)
         self.db_uncommitted = True
         return 0
 
     def do_list(self, args):
-        ""'''Usage: list classname [property]
+        ''"""Usage: list classname [property]
         List the instances of a class.
 
         Lists all instances of the given class. If the property is not
@@ -835,13 +844,13 @@ Erase it? Y/N: """))
         With -c, -S or -s print a list of item id's if no property
         specified.  If property specified, print list of that property
         for every class instance.
-        '''
+        """
         if len(args) > 2:
-            raise UsageError, _('Too many arguments supplied')
+            raise UsageError(_('Too many arguments supplied'))
         if len(args) < 1:
-            raise UsageError, _('Not enough arguments supplied')
+            raise UsageError(_('Not enough arguments supplied'))
         classname = args[0]
-
+        
         # get the class
         cl = self.get_class(classname)
 
@@ -853,14 +862,14 @@ Erase it? Y/N: """))
 
         if self.separator:
             if len(args) == 2:
-               # create a list of propnames since user specified propname
+                # create a list of propnames since user specified propname
                 proplist=[]
                 for nodeid in cl.list():
                     try:
                         proplist.append(cl.get(nodeid, propname))
                     except KeyError:
-                        raise UsageError_('%(classname)s has no property '
-                            '"%(propname)s"')%locals()
+                        raise UsageError(_('%(classname)s has no property '
+                            '"%(propname)s"')%locals())
                 print self.separator.join(proplist)
             else:
                 # create a list of index id's since user didn't specify
@@ -871,13 +880,13 @@ Erase it? Y/N: """))
                 try:
                     value = cl.get(nodeid, propname)
                 except KeyError:
-                    raise UsageError_('%(classname)s has no property '
-                        '"%(propname)s"')%locals()
+                    raise UsageError(_('%(classname)s has no property '
+                        '"%(propname)s"')%locals())
                 print _('%(nodeid)4s: %(value)s')%locals()
         return 0
 
     def do_table(self, args):
-        ""'''Usage: table classname [property[,property]*]
+        ''"""Usage: table classname [property[,property]*]
         List the instances of a class in tabular form.
 
         Lists all instances of the given class. If the properties are not
@@ -904,9 +913,9 @@ Erase it? Y/N: """))
           4  feat
 
         will result in a the 4 character wide "Name" column.
-        '''
+        """
         if len(args) < 1:
-            raise UsageError, _('Not enough arguments supplied')
+            raise UsageError(_('Not enough arguments supplied'))
         classname = args[0]
 
         # get the class
@@ -921,14 +930,15 @@ Erase it? Y/N: """))
                     try:
                         propname, width = spec.split(':')
                     except (ValueError, TypeError):
-                        raise UsageError, _('"%(spec)s" not name:width')%locals()
+                        raise UsageError(_('"%(spec)s" not '
+                            'name:width')%locals())
                 else:
                     propname = spec
-                if not all_props.has_key(propname):
-                    raise UsageError_('%(classname)s has no property '
-                        '"%(propname)s"')%locals()
+                if propname not in all_props:
+                    raise UsageError(_('%(classname)s has no property '
+                        '"%(propname)s"')%locals())
         else:
-            prop_names = cl.getprops().keys()
+            prop_names = cl.getprops()
 
         # now figure column widths
         props = []
@@ -971,28 +981,32 @@ Erase it? Y/N: """))
         return 0
 
     def do_history(self, args):
-        ""'''Usage: history designator
+        ''"""Usage: history designator
         Show the history entries of a designator.
 
+        A designator is a classname and a nodeid concatenated,
+        eg. bug1, user10, ...
+
         Lists the journal entries for the node identified by the designator.
-        '''
+        """
         if len(args) < 1:
-            raise UsageError, _('Not enough arguments supplied')
+            raise UsageError(_('Not enough arguments supplied'))
         try:
             classname, nodeid = hyperdb.splitDesignator(args[0])
         except hyperdb.DesignatorError, message:
-            raise UsageError, message
+            raise UsageError(message)
 
         try:
             print self.db.getclass(classname).history(nodeid)
         except KeyError:
-            raise UsageError, _('no such class "%(classname)s"')%locals()
+            raise UsageError(_('no such class "%(classname)s"')%locals())
         except IndexError:
-            raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
+            raise UsageError(_('no such %(classname)s node '
+                '"%(nodeid)s"')%locals())
         return 0
 
     def do_commit(self, args):
-        ""'''Usage: commit
+        ''"""Usage: commit
         Commit changes made to the database during an interactive session.
 
         The changes made during an interactive session are not
@@ -1001,73 +1015,81 @@ Erase it? Y/N: """))
 
         One-off commands on the command-line are automatically committed if
         they are successful.
-        '''
+        """
         self.db.commit()
         self.db_uncommitted = False
         return 0
 
     def do_rollback(self, args):
-        ""'''Usage: rollback
+        ''"""Usage: rollback
         Undo all changes that are pending commit to the database.
 
         The changes made during an interactive session are not
         automatically written to the database - they must be committed
         manually. This command undoes all those changes, so a commit
         immediately after would make no changes to the database.
-        '''
+        """
         self.db.rollback()
         self.db_uncommitted = False
         return 0
 
     def do_retire(self, args):
-        ""'''Usage: retire designator[,designator]*
+        ''"""Usage: retire designator[,designator]*
         Retire the node specified by designator.
 
+        A designator is a classname and a nodeid concatenated,
+        eg. bug1, user10, ...
+
         This action indicates that a particular node is not to be retrieved
         by the list or find commands, and its key value may be re-used.
-        '''
+        """
         if len(args) < 1:
-            raise UsageError, _('Not enough arguments supplied')
+            raise UsageError(_('Not enough arguments supplied'))
         designators = args[0].split(',')
         for designator in designators:
             try:
                 classname, nodeid = hyperdb.splitDesignator(designator)
             except hyperdb.DesignatorError, message:
-                raise UsageError, message
+                raise UsageError(message)
             try:
                 self.db.getclass(classname).retire(nodeid)
             except KeyError:
-                raise UsageError, _('no such class "%(classname)s"')%locals()
+                raise UsageError(_('no such class "%(classname)s"')%locals())
             except IndexError:
-                raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
+                raise UsageError(_('no such %(classname)s node '
+                    '"%(nodeid)s"')%locals())
         self.db_uncommitted = True
         return 0
 
     def do_restore(self, args):
-        ""'''Usage: restore designator[,designator]*
+        ''"""Usage: restore designator[,designator]*
         Restore the retired node specified by designator.
 
+        A designator is a classname and a nodeid concatenated,
+        eg. bug1, user10, ...
+
         The given nodes will become available for users again.
-        '''
+        """
         if len(args) < 1:
-            raise UsageError, _('Not enough arguments supplied')
+            raise UsageError(_('Not enough arguments supplied'))
         designators = args[0].split(',')
         for designator in designators:
             try:
                 classname, nodeid = hyperdb.splitDesignator(designator)
             except hyperdb.DesignatorError, message:
-                raise UsageError, message
+                raise UsageError(message)
             try:
                 self.db.getclass(classname).restore(nodeid)
             except KeyError:
-                raise UsageError, _('no such class "%(classname)s"')%locals()
+                raise UsageError(_('no such class "%(classname)s"')%locals())
             except IndexError:
-                raise UsageError, _('no such %(classname)s node "%(nodeid)s"')%locals()
+                raise UsageError(_('no such %(classname)s node '
+                    '"%(nodeid)s"')%locals())
         self.db_uncommitted = True
         return 0
 
     def do_export(self, args, export_files=True):
-        ""'''Usage: export [[-]class[,class]] export_dir
+        ''"""Usage: export [[-]class[,class]] export_dir
         Export the database to colon-separated-value files.
         To exclude the files (e.g. for the msg or file class),
         use the exporttables command.
@@ -1078,22 +1100,22 @@ Erase it? Y/N: """))
         This action exports the current data from the database into
         colon-separated-value files that are placed in the nominated
         destination directory.
-        '''
+        """
         # grab the directory to export to
         if len(args) < 1:
-            raise UsageError, _('Not enough arguments supplied')
+            raise UsageError(_('Not enough arguments supplied'))
 
         dir = args[-1]
 
         # get the list of classes to export
         if len(args) == 2:
             if args[0].startswith('-'):
-                classes = [ c for c in self.db.classes.keys()
+                classes = [ c for c in self.db.classes
                             if not c in args[0][1:].split(',') ]
             else:
                 classes = args[0].split(',')
         else:
-            classes = self.db.classes.keys()
+            classes = self.db.classes
 
         class colon_separated(csv.excel):
             delimiter = ':'
@@ -1102,6 +1124,9 @@ Erase it? Y/N: """))
         if not os.path.exists(dir):
             os.makedirs(dir)
 
+        # maximum csv field length exceeding configured size?
+        max_len = self.db.config.CSV_FIELD_SIZE
+
         # do all the classes specified
         for classname in classes:
             cl = self.get_class(classname)
@@ -1124,7 +1149,18 @@ Erase it? Y/N: """))
                 if self.verbose:
                     sys.stdout.write('\rExporting %s - %s'%(classname, nodeid))
                     sys.stdout.flush()
-                writer.writerow(cl.export_list(propnames, nodeid))
+                node = cl.getnode(nodeid)
+                exp = cl.export_list(propnames, nodeid)
+                lensum = sum ([len (repr(node[p])) for p in propnames])
+                # for a safe upper bound of field length we add
+                # difference between CSV len and sum of all field lengths
+                d = sum ([len(x) for x in exp]) - lensum
+                assert (d > 0)
+                for p in propnames:
+                    ll = len(repr(node[p])) + d
+                    if ll > max_len:
+                        max_len = ll
+                writer.writerow(exp)
                 if export_files and hasattr(cl, 'export_files'):
                     cl.export_files(dir, nodeid)
 
@@ -1137,12 +1173,16 @@ Erase it? Y/N: """))
                 sys.stdout.write("\nExporting Journal for %s\n" % classname)
                 sys.stdout.flush()
             journals = csv.writer(jf, colon_separated)
-            map(journals.writerow, cl.export_journals())
+            for row in cl.export_journals():
+                journals.writerow(row)
             jf.close()
+        if max_len > self.db.config.CSV_FIELD_SIZE:
+            print >> sys.stderr, \
+                "Warning: config csv_field_size should be at least %s"%max_len
         return 0
 
     def do_exporttables(self, args):
-        ""'''Usage: exporttables [[-]class[,class]] export_dir
+        ''"""Usage: exporttables [[-]class[,class]] export_dir
         Export the database to colon-separated-value files, excluding the
         files below $TRACKER_HOME/db/files/ (which can be archived separately).
         To include the files, use the export command.
@@ -1153,11 +1193,11 @@ Erase it? Y/N: """))
         This action exports the current data from the database into
         colon-separated-value files that are placed in the nominated
         destination directory.
-        '''
+        """
         return self.do_export(args, export_files=False)
 
     def do_import(self, args):
-        ""'''Usage: import import_dir
+        ''"""Usage: import import_dir
         Import a database from the directory containing CSV files,
         two per class to import.
 
@@ -1175,11 +1215,14 @@ Erase it? Y/N: """))
         The new nodes are added to the existing database - if you want to
         create a new database using the imported data, then create a new
         database (or, tediously, retire all the old data.)
-        '''
+        """
         if len(args) < 1:
-            raise UsageError, _('Not enough arguments supplied')
+            raise UsageError(_('Not enough arguments supplied'))
         from roundup import hyperdb
 
+        if hasattr (csv, 'field_size_limit'):
+            csv.field_size_limit(self.db.config.CSV_FIELD_SIZE)
+
         # directory to import from
         dir = args[0]
 
@@ -1215,7 +1258,10 @@ Erase it? Y/N: """))
                 if hasattr(cl, 'import_files'):
                     cl.import_files(dir, nodeid)
                 maxid = max(maxid, int(nodeid))
-            print
+
+            # (print to sys.stdout here to allow tests to squash it .. ugh)
+            print >> sys.stdout
+
             f.close()
 
             # import the journals
@@ -1224,15 +1270,17 @@ Erase it? Y/N: """))
             cl.import_journals(reader)
             f.close()
 
+            # (print to sys.stdout here to allow tests to squash it .. ugh)
+            print >> sys.stdout, 'setting', classname, maxid+1
+
             # set the id counter
-            print 'setting', classname, maxid+1
             self.db.setid(classname, str(maxid+1))
 
         self.db_uncommitted = True
         return 0
 
     def do_pack(self, args):
-        ""'''Usage: pack period | date
+        ''"""Usage: pack period | date
 
         Remove journal entries older than a period of time specified or
         before a certain date.
@@ -1248,19 +1296,19 @@ Erase it? Y/N: """))
         Date format is "YYYY-MM-DD" eg:
             2001-01-01
 
-        '''
-        if len(args) <> 1:
-            raise UsageError, _('Not enough arguments supplied')
+        """
+        if len(args) != 1:
+            raise UsageError(_('Not enough arguments supplied'))
 
         # are we dealing with a period or a date
         value = args[0]
-        date_re = re.compile(r'''
+        date_re = re.compile(r"""
               (?P<date>\d\d\d\d-\d\d?-\d\d?)? # yyyy-mm-dd
               (?P<period>(\d+y\s*)?(\d+m\s*)?(\d+d\s*)?)?
-              ''', re.VERBOSE)
+              """, re.VERBOSE)
         m = date_re.match(value)
         if not m:
-            raise ValueError, _('Invalid format')
+            raise ValueError(_('Invalid format'))
         m = m.groupdict()
         if m['period']:
             pack_before = date.Date(". - %s"%value)
@@ -1271,12 +1319,12 @@ Erase it? Y/N: """))
         return 0
 
     def do_reindex(self, args, desre=re.compile('([A-Za-z]+)([0-9]+)')):
-        ""'''Usage: reindex [classname|designator]*
+        ''"""Usage: reindex [classname|designator]*
         Re-generate a tracker's search indexes.
 
         This will re-generate the search indexes for a tracker.
         This will typically happen automatically.
-        '''
+        """
         if args:
             for arg in args:
                 m = desre.match(arg)
@@ -1285,8 +1333,8 @@ Erase it? Y/N: """))
                     try:
                         cl.index(m.group(2))
                     except IndexError:
-                        raise UsageError_('no such item "%(designator)s"')%{
-                            'designator': arg}
+                        raise UsageError(_('no such item "%(designator)s"')%{
+                            'designator': arg})
                 else:
                     cl = self.get_class(arg)
                     self.db.reindex(arg)
@@ -1295,9 +1343,9 @@ Erase it? Y/N: """))
         return 0
 
     def do_security(self, args):
-        ""'''Usage: security [Role name]
+        ''"""Usage: security [Role name]
         Display the Permissions available to one or all Roles.
-        '''
+        """
         if len(args) == 1:
             role = args[0]
             try:
@@ -1306,7 +1354,7 @@ Erase it? Y/N: """))
                 print _('No such Role "%(role)s"')%locals()
                 return 1
         else:
-            roles = self.db.security.role.items()
+            roles = list(self.db.security.role.items())
             role = self.db.config.NEW_WEB_USER_ROLES
             if ',' in role:
                 print _('New Web users get the Roles "%(role)s"')%locals()
@@ -1335,7 +1383,7 @@ Erase it? Y/N: """))
 
 
     def do_migrate(self, args):
-        '''Usage: migrate
+        ''"""Usage: migrate
         Update a tracker's database to be compatible with the Roundup
         codebase.
 
@@ -1352,7 +1400,7 @@ Erase it? Y/N: """))
 
         It's safe to run this even if it's not required, so just get into
         the habit.
-        '''
+        """
         if getattr(self.db, 'db_version_updated'):
             print _('Tracker updated')
             self.db_uncommitted = True
@@ -1361,8 +1409,8 @@ Erase it? Y/N: """))
         return 0
 
     def run_command(self, args):
-        '''Run a single command
-        '''
+        """Run a single command
+        """
         command = args[0]
 
         # handle help now
@@ -1443,8 +1491,8 @@ Erase it? Y/N: """))
         return ret
 
     def interactive(self):
-        '''Run in an interactive mode
-        '''
+        """Run in an interactive mode
+        """
         print _('Roundup %s ready for input.\nType "help" for help.'
             % roundup_version)
         try:
@@ -1482,7 +1530,7 @@ Erase it? Y/N: """))
         self.tracker_home = os.environ.get('TRACKER_HOME', '')
         # TODO: reinstate the user/password stuff (-u arg too)
         name = password = ''
-        if os.environ.has_key('ROUNDUP_LOGIN'):
+        if 'ROUNDUP_LOGIN' in os.environ:
             l = os.environ['ROUNDUP_LOGIN'].split(':')
             name = l[0]
             if len(l) > 1: