From: Fred L. D. <fd...@us...> - 2003-07-10 05:40:14
|
Update of /cvsroot/cvs-syncmail/syncmail In directory sc8-pr-cvs1:/tmp/cvs-serv16195 Modified Files: Tag: new-config-branch syncmail Added Files: Tag: new-config-branch syncmail.conf tests.py Log Message: Checkpoint work on the new configuration support. Not currently usable, but I want to make sure I do not lose the changes. --- NEW FILE: tests.py --- #! /usr/bin/env python """Tests for the helper functions in syncmail.""" # These tests assume that the syncmail script is in the current directory. import os # Since there's no .py extension, we need to load syncmail magically # so we don't trigger the main() function: __name__ = "notmain" execfile("syncmail") VARS = {"FOO": "<whack!>"} def eq(a, b, msg=None): if msg is None: msg = "%s != %s" % (`a`, `b`) assert a == b, msg replace = Replacer(VARS) eq(replace("abc$FOO-def${SPLAT}"), "abc<whack!>-def") eq(replace("$FOO"), "<whack!>") config, args = load_configuration([]) eq(config.getint("context-lines"), 2) eq(config.getbool("verbose"), 1) eq(config.getaddress("smtp-server"), (MAILHOST, MAILPORT)) config, args = load_configuration(['-q', '--mailhost=smtp.example.com']) eq(config.getint("context-lines"), 2) eq(config.getbool("verbose"), 0) eq(config.getaddress("smtp-server"), ("smtp.example.com", MAILPORT)) config, args = load_configuration(['--mailhost=smtp.example.com:8025']) eq(config.getaddress("smtp-server"), ("smtp.example.com", 8025)) config, args = load_configuration(['--mailhost=:8025']) eq(config.getaddress("smtp-server"), (MAILHOST, 8025)) dicts = [ {'common': '1', 'first': 'one'}, {'common': '2', 'second': 'two'}, {'common': '3', 'third': 'three'}, {'common': '4', 'fourth': 'four'}, ] options = OptionLookup(dicts) eq(options.get('common'), '1') eq(options.get('first'), 'one') eq(options.get('second'), 'two') eq(options.get('third'), 'three') eq(options.get('fourth'), 'four') eq(options.get('missing'), None) eq(options.get('common', 'splat'), '1') eq(options.get('third', 'foo'), 'three') options = OptionLookup([{"foo": "bar", "branch": "$BRANCH", "hostname": "$HOSTNAME"}], "my-branch") eq(options.get("branch"), "my-branch") eq(options.get("hostname"), os.environ.get("HOSTNAME", getfqdn())) --- NEW FILE: syncmail.conf --- ; In values, substitutions for $BRANCH, $CVSROOT, and $HOSTNAME are ; available. These may also be spelled with curlies (for example: ; ${BRANCH}). These are replaced by the branch name (or the empty ; string for the trunk), the CVSROOT environment variable (not the ; root specified using --cvsroot), and the hostname (from the HOSTNAME ; environment variable or, if not set, the fully-qualified hostname). [general] ; miscellaneous cvsroot = $CVSROOT verbose = true email = true ; the kind of diff we generate context-lines = 2 diff-type = unified ; how email is generated from-host = $HOSTNAME reply-to = smtp-server = localhost subject-prefix = to = cvs...@li... [branch] ; the * branch applies to all branches subject-prefix = $BRANCH: [branch my-project-branch] email = false [branch another-branch] subject-prefix = [Special] to = my...@ex... Index: syncmail =================================================================== RCS file: /cvsroot/cvs-syncmail/syncmail/syncmail,v retrieving revision 1.36 retrieving revision 1.36.2.1 diff -u -d -r1.36 -r1.36.2.1 --- syncmail 9 Jul 2003 23:13:37 -0000 1.36 +++ syncmail 10 Jul 2003 05:40:10 -0000 1.36.2.1 @@ -159,7 +159,7 @@ if oldrev is None and newrev is None: return NOVERSION % file - if string.find(file, "'") <> -1: + if "'" in file: # Those crazy users put single-quotes in their file names! Now we # have to escape everything that is meaningful inside double-quotes. filestr = string.replace(file, '\\', '\\\\') @@ -250,7 +250,7 @@ try: vars = {'address' : address, 'name' : quotename(name), - 'people' : string.join(people, COMMASPACE), + 'people' : people, 'subject' : subject, 'version' : __version__, 'date' : datestamp, @@ -297,7 +297,7 @@ except KeyError: if revision == "0": revision = None - if string.find(timestamp, "+") != -1: + if "+" in timestamp: timestamp, conflict = tuple(string.split(timestamp, "+")) else: conflict = None @@ -374,50 +374,210 @@ return line[1:] return None -# scan args for options -def main(): - # XXX Should really move all the options to an object, just to - # avoid threading so many positional args through everything. + +TRUE_VALUES = ('true', 'on', 'enabled') +FALSE_VALUES = ('false', 'off', 'disabled') + +class OptionLookup: + def __init__(self, dicts, branch=None): + self._dicts = dicts + self._replace = Replacer({ + "BRANCH": branch or "", + "CVSROOT": os.environ.get("CVSROOT", ""), + "HOSTNAME": os.environ.get("HOSTNAME") or getfqdn(), + }) + + def get(self, option, default=None): + for dict in self._dicts: + v = dict.get(option) + if v is not None: + return self._replace(v) + return default + + def getbool(self, option, default=None): + v = self.get(option) + if v is None: + return default + v = string.lower(v) + if v in TRUE_VALUES: + return 1 + elif v in FALSE_VALUES: + return 0 + else: + raise ValueError("illegal boolean value: %s" % `v`) + + def getint(self, option, default=None): + v = self.get(option) + if v is None: + return default + else: + return int(v) + + def getaddress(self, option): + """Return (host, port) for a host:port or host string. + + The port, if ommitted, will be None. + """ + v = self.get(option) + if v is None: + return MAILHOST, MAILPORT + elif ":" in v: + h, p = tuple(string.split(v, ":")) + p = int(p) + return h or MAILHOST, p + else: + return v, MAILPORT + +# Support for $VARIABLE replacement. + +class Replacer: + def __init__(self, vars): + self._vars = vars + rx = re.compile(r"\$([a-zA-Z][a-zA-Z_]*\b|\{[a-zA-Z][a-zA-Z_]*\})") + self._search = rx.search + + def __call__(self, v): + v, name, suffix = self._split(v) + while name: + v = v + self._vars.get(name, "") + prefix, name, suffix = self._split(suffix) + v = v + prefix + return v + + def _split(self, s): + m = self._search(s) + if m is not None: + name = m.group(1) + if name[0] == "{": + name = name[1:-1] + return s[:m.start()], name, s[m.end():] + else: + return s, None, '' + +def get_section_as_dict(config, section): + d = {} + if config.has_section(section): + for opt in config.options(section): + d[opt] = config.get(section, opt, raw=1) + return d + +def load_configfile(filename, cmdline, branch): + dicts = [] + if filename: + from ConfigParser import ConfigParser + class ConfigParser(ConfigParser): + # Regular expressions for parsing section headers and options, + # from the Python 2.3 version of ConfigParser. + SECTCRE = re.compile( + r'\[' # [ + r'(?P<header>[^]]+)' # very permissive! + r'\]' # ] + ) + OPTCRE = re.compile( + r'(?P<option>[^:=\s][^:=]*)' # very permissive! + r'\s*(?P<vi>[:=])\s*' # any number of space/tab, + # followed by separator + # (either : or =), followed + # by any # space/tab + r'(?P<value>.*)$' # everything up to eol + ) + # For compatibility with older versions: + __SECTCRE = SECTCRE + __OPTCRE = OPTCRE + + cp = ConfigParser() + # We have to use this old method for compatibility with + # ancient versions of Python. + cp.read([filename]) + if branch: + dicts.append(get_section_as_dict(cp, "branch " + branch)) + dicts.append(get_section_as_dict(cp, "branch")) + dicts.append(cmdline) + dicts.append(get_section_as_dict(cp, "general")) + else: + dicts.append(cmdline) + dicts = filter(None, dicts) + # The defaults set covers what we need but might not get from the + # command line or configuration file. + defaults = { + "context-lines": "2", + "cvsroot": "$CVSROOT", + "diff-type": "unified", + "email": "true", + "from-host": "$HOSTNAME", + "smtp-server": "localhost", + "smtp-server": "localhost", + "subject-prefix": "", + "verbose": "true", + } + dicts.append(defaults) + return OptionLookup(dicts, branch) + +def load_cmdline(args): try: - opts, args = getopt.getopt( - sys.argv[1:], 'hC:cuS:R:qf:m:', - ['fromhost=', 'context=', 'cvsroot=', 'mailhost=', - 'subject-prefix=', 'reply-to=', - 'help', 'quiet']) + opts, args = getopt.getopt(args, + 'hC:cuS:R:qf:m:', + ['fromhost=', 'context=', 'cvsroot=', + 'mailhost=', 'subject-prefix=', + 'reply-to=', 'help', 'quiet']) except getopt.error, msg: - usage(1, msg) - - # parse the options - contextlines = 2 - verbose = 1 - subject_prefix = "" - replyto = None - fromhost = None + usage(2, msg) + cmdline = {"config-file": "syncmail.conf"} for opt, arg in opts: if opt in ('-h', '--help'): usage(0) elif opt == '--cvsroot': - os.environ['CVSROOT'] = arg + cmdline['cvsroot'] = arg + elif opt == "--config": + if arg == '-' and cmdline.has_key('config-file'): + del cmdline['config-file'] + else: + cmdline['config-file'] = arg elif opt in ('-C', '--context'): - contextlines = int(arg) + cmdline['context-lines'] = arg elif opt == '-c': - if contextlines <= 0: - contextlines = 2 + cmdline['diff-type'] = 'context' elif opt == '-u': - contextlines = 0 + cmdline['diff-type'] = 'unified' elif opt in ('-S', '--subject-prefix'): - subject_prefix = arg + cmdline['subject-prefix'] = arg elif opt in ('-R', '--reply-to'): - replyto = arg + cmdline['reply-to'] = arg elif opt in ('-q', '--quiet'): - verbose = 0 + cmdline['verbose'] = 'false' elif opt in ('-f', '--fromhost'): - fromhost = arg + cmdline['from-host'] = arg elif opt in ('-m', '--mailhost'): - global MAILHOST - MAILHOST = arg + cmdline['smtp-server'] = arg + return cmdline, args - # What follows is the specification containing the files that were +def load_configuration(args, branch=None): + cmdline, args = load_cmdline(args) + cfgfile = cmdline.get('config-file') + # cfgfile is specified relative to the CVSROOT directory; we need + # to transform the path appropriately, since that won't be the + # current directory when we try to read it. In fact, a wrong + # config file may exist at the alternate location. + return load_configfile(cfgfile, cmdline, branch), args + + + +def main(): + # XXX Should really move all the options to an object, just to + # avoid threading so many positional args through everything. + + # load the options + config, args = load_configuration(sys.argv[1:], load_branch_name()) + contextlines = config.getint('context-lines') + verbose = config.getbool('verbose') + subject_prefix = config.get('subject-prefix') + replyto = config.get('reply-to') + fromhost = config.get('from-host') + email = config.getbool('email') + global MAILHOST, MAILPORT + MAILHOST, MAILPORT = config.getaddress('smtp-server') + + # args[0] is the specification containing the files that were # modified. The argument actually must be split, with the first component # containing the directory the checkin is being made in, relative to # $CVSROOT, followed by the list of files that are changing. @@ -428,21 +588,20 @@ del args[0] # The remaining args should be the email addresses - if not args: - usage(1, 'No recipients specified') - - # Now do the mail command - people = args + if args: + people = string.join(args, COMMASPACE) + else: + people = config.get("to") if specs[-3:] == ['-', 'Imported', 'sources']: + # What to do here should be configurable. print 'Not sending email for imported sources.' return - branch = load_branch_name() changes = load_change_info() if verbose: - print 'Mailing %s...' % string.join(people, COMMASPACE) + print 'Mailing %s...' % people print 'Generating notification message...' blast_mail(subject, people, changes.values(), contextlines, fromhost, replyto) @@ -453,4 +612,3 @@ if __name__ == '__main__': main() - sys.exit(0) |