|
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)
|