From: richard Date: Tue, 11 Nov 2003 00:35:14 +0000 (+0000) Subject: Centralised conversion of user-input data to hyperdb values (bug #802405, X-Git-Url: https://git.tokkee.org/?a=commitdiff_plain;h=28c868f2f2370ccd0edac1f1212ee1cc141ea892;p=roundup.git Centralised conversion of user-input data to hyperdb values (bug #802405, bug #817217, rfe #816994) git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@1974 57a73879-2fb5-44c3-a270-3262357dd7e2 --- diff --git a/CHANGES.txt b/CHANGES.txt index ee5818e..fed3b75 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -28,6 +28,8 @@ Fixed: - Date arithmetic was utterly broken, and has been for a long time. Date +/- Interval now works, and Date - Date also works (produces an Interval. +- Centralised conversion of user-input data to hyperdb values (bug #802405, + bug #817217, rfe #816994) Cleanup: - Replace curuserid attribute on Database with the extended getuid() method. diff --git a/roundup/admin.py b/roundup/admin.py index 413509a..107e639 100644 --- a/roundup/admin.py +++ b/roundup/admin.py @@ -16,7 +16,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: admin.py,v 1.59 2003-10-24 19:48:05 jlgijsbers Exp $ +# $Id: admin.py,v 1.60 2003-11-11 00:35:13 richard Exp $ '''Administration commands for maintaining Roundup trackers. ''' @@ -569,42 +569,11 @@ Command help: properties = cl.getprops() for key, value in props.items(): - proptype = properties[key] - if isinstance(proptype, hyperdb.Multilink): - if value is None: - props[key] = [] - else: - props[key] = value.split(',') - elif value is None: - continue - elif isinstance(proptype, hyperdb.String): - continue - elif isinstance(proptype, hyperdb.Password): - m = pwre.match(value) - if m: - # password is being given to us encrypted - p = password.Password() - p.scheme = m.group(1) - p.password = m.group(2) - props[key] = p - else: - props[key] = password.Password(value) - elif isinstance(proptype, hyperdb.Date): - try: - props[key] = date.Date(value) - except ValueError, message: - raise UsageError, '"%s": %s'%(value, message) - elif isinstance(proptype, hyperdb.Interval): - try: - props[key] = date.Interval(value) - except ValueError, message: - raise UsageError, '"%s": %s'%(value, message) - elif isinstance(proptype, hyperdb.Link): - props[key] = value - elif isinstance(proptype, hyperdb.Boolean): - props[key] = value.lower() in ('yes', 'true', 'on', '1') - elif isinstance(proptype, hyperdb.Number): - props[key] = float(value) + try: + props[key] = hyperdb.rawToHyperdb(self.db, cl, itemid, + key, value) + except hyperdb.HyperdbValueError, message: + raise UsageError, message # try the set try: @@ -777,39 +746,11 @@ Command help: # convert types for propname, value in props.items(): - # get the property try: - proptype = properties[propname] - except KeyError: - raise UsageError, _('%(classname)s has no property ' - '"%(propname)s"')%locals() - - if isinstance(proptype, hyperdb.Date): - try: - props[propname] = date.Date(value) - except ValueError, message: - raise UsageError, _('"%(value)s": %(message)s')%locals() - elif isinstance(proptype, hyperdb.Interval): - try: - props[propname] = date.Interval(value) - except ValueError, message: - raise UsageError, _('"%(value)s": %(message)s')%locals() - elif isinstance(proptype, hyperdb.Password): - m = pwre.match(value) - if m: - # password is being given to us encrypted - p = password.Password() - p.scheme = m.group(1) - p.password = m.group(2) - props[propname] = p - else: - props[propname] = password.Password(value) - elif isinstance(proptype, hyperdb.Multilink): - props[propname] = value.split(',') - elif isinstance(proptype, hyperdb.Boolean): - props[propname] = value.lower() in ('yes', 'true', 'on', '1') - elif isinstance(proptype, hyperdb.Number): - props[propname] = float(value) + props[key] = hyperdb.rawToHyperdb(self.db, cl, None, + propname, value) + except hyperdb.HyperdbValueError, message: + raise UsageError, message # check for the key property propname = cl.getkey() diff --git a/roundup/cgi/client.py b/roundup/cgi/client.py index 72edc6f..5e581b0 100644 --- a/roundup/cgi/client.py +++ b/roundup/cgi/client.py @@ -1,4 +1,4 @@ -# $Id: client.py,v 1.143 2003-10-24 09:32:19 jlgijsbers Exp $ +# $Id: client.py,v 1.144 2003-11-11 00:35:14 richard Exp $ __doc__ = """ WWW request handler (also used in the stand-alone server). @@ -1675,51 +1675,18 @@ You should then receive another email with the new password. if value != confirm.value: raise FormError, 'Password and confirmation text do '\ 'not match' - value = password.Password(value) - - elif isinstance(proptype, hyperdb.Link): - # see if it's the "no selection" choice - if value == '-1' or not value: - # if we're creating, just don't include this property - if not nodeid or nodeid.startswith('-'): - continue - value = None - else: - # handle key values - link = proptype.classname - if not num_re.match(value): - try: - value = db.classes[link].lookup(value) - except KeyError: - raise FormError, _('property "%(propname)s": ' - '%(value)s not a %(classname)s')%{ - 'propname': propname, 'value': value, - 'classname': link} - except TypeError, message: - raise FormError, _('you may only enter ID values ' - 'for property "%(propname)s": %(message)s')%{ - 'propname': propname, 'message': message} + try: + value = password.Password(value) + except hyperdb.HyperdbValueError, msg: + raise FormError, msg + elif isinstance(proptype, hyperdb.Multilink): - # perform link class key value lookup if necessary - link = proptype.classname - link_cl = db.classes[link] - l = [] - for entry in value: - if not entry: continue - if not num_re.match(entry): - try: - entry = link_cl.lookup(entry) - except KeyError: - raise FormError, _('property "%(propname)s": ' - '"%(value)s" not an entry of %(classname)s')%{ - 'propname': propname, 'value': entry, - 'classname': link} - except TypeError, message: - raise FormError, _('you may only enter ID values ' - 'for property "%(propname)s": %(message)s')%{ - 'propname': propname, 'message': message} - l.append(entry) - l.sort() + # convert input to list of ids + try: + l = hyperdb.rawToHyperdb(self.db, cl, nodeid, + propname, value) + except hyperdb.HyperdbValueError, msg: + raise FormError, msg # now use that list of ids to modify the multilink if mlaction == 'set': @@ -1753,13 +1720,10 @@ You should then receive another email with the new password. value.sort() elif value == '': - # if we're creating, just don't include this property - if not nodeid or nodeid.startswith('-'): - continue # other types should be None'd if there's no value value = None else: - # handle ValueErrors for all these in a similar fashion + # handle all other types try: if isinstance(proptype, hyperdb.String): if (hasattr(value, 'filename') and @@ -1777,23 +1741,17 @@ You should then receive another email with the new password. props['type'] = mimetypes.guess_type(fn)[0] if not props['type']: props['type'] = "application/octet-stream" - # finally, read the content + # finally, read the content RAW value = value.value else: - # normal String fix the CRLF/CR -> LF stuff - value = fixNewlines(value) + value = hyperdb.rawToHyperdb(self.db, cl, + nodeid, propname, value) - elif isinstance(proptype, hyperdb.Date): - value = date.Date(value, offset=timezone) - elif isinstance(proptype, hyperdb.Interval): - value = date.Interval(value) - elif isinstance(proptype, hyperdb.Boolean): - value = value.lower() in ('yes', 'true', 'on', '1') - elif isinstance(proptype, hyperdb.Number): - value = float(value) - except ValueError, msg: - raise FormError, _('Error with %s property: %s')%( - propname, msg) + else: + value = hyperdb.rawToHyperdb(self.db, cl, nodeid, + propname, value) + except hyperdb.HyperdbValueError, msg: + raise FormError, msg # register that we got this property if value: @@ -1881,16 +1839,6 @@ You should then receive another email with the new password. raise FormError, _('File is empty') return all_props, all_links -def fixNewlines(text): - ''' Homogenise line endings. - - Different web clients send different line ending values, but - other systems (eg. email) don't necessarily handle those line - endings. Our solution is to convert all line endings to LF. - ''' - text = text.replace('\r\n', '\n') - return text.replace('\r', '\n') - def extractFormList(value): ''' Extract a list of values from the form value. diff --git a/roundup/hyperdb.py b/roundup/hyperdb.py index 6a1c831..245b917 100644 --- a/roundup/hyperdb.py +++ b/roundup/hyperdb.py @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: hyperdb.py,v 1.90 2003-10-24 22:52:48 richard Exp $ +# $Id: hyperdb.py,v 1.91 2003-11-11 00:35:13 richard Exp $ """ Hyperdatabase implementation, especially field types. @@ -572,6 +572,171 @@ class Class: ''' raise NotImplementedError +class HyperdbValueError(ValueError): + ''' Error converting a raw value into a Hyperdb value ''' + pass + +def convertLinkValue(db, propname, prop, value, idre=re.compile('\d+')): + ''' Convert the link value (may be id or key value) to an id value. ''' + linkcl = db.classes[prop.classname] + if not idre.match(value): + if linkcl.getkey(): + try: + value = linkcl.lookup(value) + except KeyError, message: + raise HyperdbValueError, 'property %s: %r is not a %s.'%( + propname, value, prop.classname) + else: + raise HyperdbValueError, 'you may only enter ID values '\ + 'for property %s'%propname + return value + +def fixNewlines(text): + ''' Homogenise line endings. + + Different web clients send different line ending values, but + other systems (eg. email) don't necessarily handle those line + endings. Our solution is to convert all line endings to LF. + ''' + text = text.replace('\r\n', '\n') + return text.replace('\r', '\n') + +def rawToHyperdb(db, klass, itemid, propname, value, + pwre=re.compile(r'{(\w+)}(.+)')): + ''' Convert the raw (user-input) value to a hyperdb-storable value. The + value is for the "propname" property on itemid (may be None for a + new item) of "klass" in "db". + + The value is usually a string, but in the case of multilink inputs + it may be either a list of strings or a string with comma-separated + values. + ''' + properties = klass.getprops() + + # ensure it's a valid property name + propname = propname.strip() + try: + proptype = properties[propname] + except KeyError: + raise HyperdbValueError, '%r is not a property of %s'%(propname, + klass.classname) + + # if we got a string, strip it now + if isinstance(value, type('')): + value = value.strip() + + # convert the input value to a real property value + if isinstance(proptype, String): + # fix the CRLF/CR -> LF stuff + value = fixNewlines(value) + if isinstance(proptype, Password): + m = pwre.match(value) + if m: + # password is being given to us encrypted + p = password.Password() + p.scheme = m.group(1) + if p.scheme not in 'SHA crypt plaintext'.split(): + raise HyperdbValueError, 'property %s: unknown encryption '\ + 'scheme %r'%(propname, p.scheme) + p.password = m.group(2) + value = p + else: + try: + value = password.Password(value) + except password.PasswordValueError, message: + raise HyperdbValueError, 'property %s: %s'%(propname, message) + elif isinstance(proptype, Date): + try: + tz = db.getUserTimezone() + value = date.Date(value).local(tz) + except ValueError, message: + raise HyperdbValueError, 'property %s: %r is an invalid '\ + 'date (%s)'%(propname, value, message) + elif isinstance(proptype, Interval): + try: + value = date.Interval(value) + except ValueError, message: + raise HyperdbValueError, 'property %s: %r is an invalid '\ + 'date interval (%s)'%(propname, value, message) + elif isinstance(proptype, Link): + if value == '-1' or not value: + value = None + else: + value = convertLinkValue(db, propname, proptype, value) + + elif isinstance(proptype, Multilink): + # get the current item value if it's not a new item + if itemid and not itemid.startswith('-'): + curvalue = klass.get(itemid, propname) + else: + curvalue = [] + + # if the value is a comma-separated string then split it now + if isinstance(value, type('')): + value = value.split(',') + + # handle each add/remove in turn + # keep an extra list for all items that are + # definitely in the new list (in case of e.g. + # =A,+B, which should replace the old + # list with A,B) + set = 1 + newvalue = [] + for item in value: + item = item.strip() + + # skip blanks + if not item: continue + + # handle +/- + remove = 0 + if item.startswith('-'): + remove = 1 + item = item[1:] + set = 0 + elif item.startswith('+'): + item = item[1:] + set = 0 + + # look up the value + itemid = convertLinkValue(db, propname, proptype, item) + + # perform the add/remove + if remove: + try: + curvalue.remove(itemid) + except ValueError: + raise HyperdbValueError, 'property %s: %r is not ' \ + 'currently an element'%(propname, item) + else: + newvalue.append(itemid) + if itemid not in curvalue: + curvalue.append(itemid) + + # that's it, set the new Multilink property value, + # or overwrite it completely + if set: + value = newvalue + else: + value = curvalue + + # TODO: one day, we'll switch to numeric ids and this will be + # unnecessary :( + value = [int(x) for x in value] + value.sort() + value = [str(x) for x in value] + elif isinstance(proptype, Boolean): + value = value.strip() + value = value.lower() in ('yes', 'true', 'on', '1') + elif isinstance(proptype, Number): + value = value.strip() + try: + value = float(value) + except ValueError: + raise HyperdbValueError, 'property %s: %r is not a number'%( + propname, value) + return value + class FileClass: ''' A class that requires the "content" property and stores it on disk. diff --git a/roundup/mailgw.py b/roundup/mailgw.py index ab03bda..a45f9e9 100644 --- a/roundup/mailgw.py +++ b/roundup/mailgw.py @@ -73,7 +73,7 @@ are calling the create() method to create a new node). If an auditor raises an exception, the original message is bounced back to the sender with the explanatory message given in the exception. -$Id: mailgw.py,v 1.136 2003-11-03 18:34:03 jlgijsbers Exp $ +$Id: mailgw.py,v 1.137 2003-11-11 00:35:13 richard Exp $ """ import string, re, os, mimetools, cStringIO, smtplib, socket, binascii, quopri @@ -851,11 +851,10 @@ There was a problem with the message you sent: return nodeid -def setPropArrayFromString(self, cl, propString, nodeid = None): +def setPropArrayFromString(self, cl, propString, nodeid=None): ''' takes string of form prop=value,value;prop2=value and returns (error, prop[..]) ''' - properties = cl.getprops() props = {} errors = [] for prop in string.split(propString, ';'): @@ -866,100 +865,13 @@ def setPropArrayFromString(self, cl, propString, nodeid = None): errors.append('not of form [arg=value,value,...;' 'arg=value,value,...]') return (errors, props) - - # ensure it's a valid property name + # convert the value to a hyperdb-usable value propname = propname.strip() try: - proptype = properties[propname] - except KeyError: - errors.append('refers to an invalid property: "%s"'%propname) - continue - - # convert the string value to a real property value - if isinstance(proptype, hyperdb.String): - props[propname] = value.strip() - if isinstance(proptype, hyperdb.Password): - props[propname] = password.Password(value.strip()) - elif isinstance(proptype, hyperdb.Date): - try: - props[propname] = date.Date(value.strip()).local(self.db.getUserTimezone()) - except ValueError, message: - errors.append('contains an invalid date for %s.'%propname) - elif isinstance(proptype, hyperdb.Interval): - try: - props[propname] = date.Interval(value) - except ValueError, message: - errors.append('contains an invalid date interval for %s.'% - propname) - elif isinstance(proptype, hyperdb.Link): - linkcl = self.db.classes[proptype.classname] - propkey = linkcl.labelprop(default_to_id=1) - try: - props[propname] = linkcl.lookup(value) - except KeyError, message: - errors.append('"%s" is not a value for %s.'%(value, propname)) - elif isinstance(proptype, hyperdb.Multilink): - # get the linked class - linkcl = self.db.classes[proptype.classname] - propkey = linkcl.labelprop(default_to_id=1) - if nodeid: - curvalue = cl.get(nodeid, propname) - else: - curvalue = [] - - # handle each add/remove in turn - # keep an extra list for all items that are - # definitely in the new list (in case of e.g. - # =A,+B, which should replace the old - # list with A,B) - set = 0 - newvalue = [] - for item in value.split(','): - item = item.strip() - - # handle +/- - remove = 0 - if item.startswith('-'): - remove = 1 - item = item[1:] - elif item.startswith('+'): - item = item[1:] - else: - set = 1 - - # look up the value - try: - item = linkcl.lookup(item) - except KeyError, message: - errors.append('"%s" is not a value for %s.'%(item, - propname)) - continue - - # perform the add/remove - if remove: - try: - curvalue.remove(item) - except ValueError: - errors.append('"%s" is not currently in for %s.'%(item, - propname)) - continue - else: - newvalue.append(item) - if item not in curvalue: - curvalue.append(item) - - # that's it, set the new Multilink property value, - # or overwrite it completely - if set: - props[propname] = newvalue - else: - props[propname] = curvalue - elif isinstance(proptype, hyperdb.Boolean): - value = value.strip() - props[propname] = value.lower() in ('yes', 'true', 'on', '1') - elif isinstance(proptype, hyperdb.Number): - value = value.strip() - props[propname] = float(value) + props[propname] = hyperdb.rawToHyperdb(self.db, cl, nodeid, + propname, value) + except hyperdb.HyperdbValueError, message: + errors.append(message) return errors, props diff --git a/roundup/password.py b/roundup/password.py index a2f01f5..97de75d 100644 --- a/roundup/password.py +++ b/roundup/password.py @@ -15,7 +15,7 @@ # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. # -# $Id: password.py,v 1.9 2003-04-10 05:12:41 richard Exp $ +# $Id: password.py,v 1.10 2003-11-11 00:35:13 richard Exp $ __doc__ = """ Password handling (encoding, decoding). @@ -28,6 +28,10 @@ except: crypt = None pass +class PasswordValueError(ValueError): + ''' The password value is not valid ''' + pass + def encodePassword(plaintext, scheme, other=None): '''Encrypt the plaintext password. ''' @@ -45,7 +49,7 @@ def encodePassword(plaintext, scheme, other=None): elif scheme == 'plaintext': s = plaintext else: - raise ValueError, 'Unknown encryption scheme "%s"'%scheme + raise PasswordValueError, 'unknown encryption scheme %r'%scheme return s def generatePassword(length=8): diff --git a/test/test_hyperdbvals.py b/test/test_hyperdbvals.py new file mode 100644 index 0000000..b99f982 --- /dev/null +++ b/test/test_hyperdbvals.py @@ -0,0 +1,129 @@ +# +# Copyright (c) 2003 Richard Jones, richard@commonground.com.au +# This module is free software, and you may redistribute it and/or modify +# under the same terms as Python, so long as this copyright message and +# disclaimer are retained in their original form. +# +# This module is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# +# $Id: test_hyperdbvals.py,v 1.1 2003-11-11 00:35:14 richard Exp $ + +import unittest, os, shutil, errno, sys, difflib, cgi, re, sha + +from roundup import init, instance, password, hyperdb, date + +class TestClass: + def getprops(self): + return { + 'string': hyperdb.String(), + 'number': hyperdb.Number(), + 'boolean': hyperdb.Boolean(), + 'password': hyperdb.Password(), + 'date': hyperdb.Date(), + 'interval': hyperdb.Interval(), + 'link': hyperdb.Link('test'), + 'link2': hyperdb.Link('test2'), + 'multilink': hyperdb.Multilink('test'), + 'multilink2': hyperdb.Multilink('test2'), + } + def getkey(self): + return 'string' + def lookup(self, value): + if value == 'valid': + return '1' + raise KeyError + def get(self, nodeid, propname): + assert propname.startswith('multilink') + assert nodeid is not None + return ['2', '3'] + +class TestClass2: + def properties(self): + return { + 'string': hyperdb.String(), + } + def getkey(self): + return None + def labelprop(self, default_to_id=1): + return 'id' + +class TestDatabase: + classes = {'test': TestClass(), 'test2': TestClass2()} + def getUserTimezone(self): + return 0 + +class RawToHyperdbTest(unittest.TestCase): + def _test(self, propname, value, itemid=None): + return hyperdb.rawToHyperdb(TestDatabase(), TestClass(), itemid, + propname, value) + def testString(self): + self.assertEqual(self._test('string', ' a string '), 'a string') + def testNumber(self): + self.assertEqual(self._test('number', ' 10 '), 10) + self.assertEqual(self._test('number', ' 1.5 '), 1.5) + def testBoolean(self): + for true in 'yes true on 1'.split(): + self.assertEqual(self._test('boolean', ' %s '%true), 1) + for false in 'no false off 0'.split(): + self.assertEqual(self._test('boolean', ' %s '%false), 0) + def testPassword(self): + self.assertEqual(self._test('password', ' a string '), 'a string') + val = self._test('password', ' a string ') + self.assert_(isinstance(val, password.Password)) + val = self._test('password', '{plaintext}a string') + self.assert_(isinstance(val, password.Password)) + val = self._test('password', '{crypt}a string') + self.assert_(isinstance(val, password.Password)) + s = sha.sha('a string').hexdigest() + val = self._test('password', '{SHA}'+s) + self.assert_(isinstance(val, password.Password)) + self.assertEqual(val, 'a string') + self.assertRaises(hyperdb.HyperdbValueError, self._test, + 'password', '{fubar}a string') + def testDate(self): + val = self._test('date', ' 2003-01-01 ') + self.assert_(isinstance(val, date.Date)) + val = self._test('date', ' 2003/01/01 ') + self.assert_(isinstance(val, date.Date)) + val = self._test('date', ' 2003/1/1 ') + self.assert_(isinstance(val, date.Date)) + val = self._test('date', ' 2003-1-1 ') + self.assert_(isinstance(val, date.Date)) + self.assertRaises(hyperdb.HyperdbValueError, self._test, 'date', + 'fubar') + def testInterval(self): + val = self._test('interval', ' +1d ') + self.assert_(isinstance(val, date.Interval)) + self.assertRaises(hyperdb.HyperdbValueError, self._test, 'interval', + 'fubar') + def testLink(self): + self.assertEqual(self._test('link', '1'), '1') + self.assertEqual(self._test('link', 'valid'), '1') + self.assertRaises(hyperdb.HyperdbValueError, self._test, 'link', + 'invalid') + def testMultilink(self): + self.assertEqual(self._test('multilink', '', '1'), []) + self.assertEqual(self._test('multilink', '1', '1'), ['1']) + self.assertEqual(self._test('multilink', 'valid', '1'), ['1']) + self.assertRaises(hyperdb.HyperdbValueError, self._test, 'multilink', + 'invalid', '1') + self.assertEqual(self._test('multilink', '+1', '1'), ['1', '2', '3']) + self.assertEqual(self._test('multilink', '+valid', '1'), ['1', '2', + '3']) + self.assertEqual(self._test('multilink', '+1,-2', '1'), ['1', '3']) + self.assertEqual(self._test('multilink', '+valid,-3', '1'), ['1', '2']) + self.assertEqual(self._test('multilink', '+1', None), ['1']) + self.assertEqual(self._test('multilink', '+valid', None), ['1']) + self.assertEqual(self._test('multilink', '', None), []) + +def test_suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(RawToHyperdbTest)) + return suite + +if __name__ == '__main__': + runner = unittest.TextTestRunner() + unittest.main(testRunner=runner) +# vim: set filetype=python ts=4 sw=4 et si diff --git a/test/test_mailgw.py b/test/test_mailgw.py index b8019c3..211b4c4 100644 --- a/test/test_mailgw.py +++ b/test/test_mailgw.py @@ -8,7 +8,7 @@ # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # -# $Id: test_mailgw.py,v 1.60 2003-11-03 22:23:02 jlgijsbers Exp $ +# $Id: test_mailgw.py,v 1.61 2003-11-11 00:35:14 richard Exp $ import unittest, tempfile, os, shutil, errno, imp, sys, difflib, rfc822 @@ -93,7 +93,7 @@ class MailgwTestCase(unittest.TestCase, DiffHelper): init.install(self.dirname, 'templates/classic') init.write_select_db(self.dirname, 'anydbm') init.initialise(self.dirname, 'sekrit') - + # check we can load the package self.instance = instance.open(self.dirname)