Added local copy of ConfigParser.py to make berkwood Windows 1.3 binary Mercurial distro work.
diff --git a/infocalypse/__init__.py b/infocalypse/__init__.py --- a/infocalypse/__init__.py +++ b/infocalypse/__init__.py @@ -297,7 +297,8 @@ from infcmds import get_config_info, exe from fmscmds import execute_fmsread, execute_fmsnotify -from sitecmds import read_freesite_cfg, execute_putsite, execute_genkey +from sitecmds import execute_putsite, execute_genkey +from config import read_freesite_cfg def set_target_version(ui_, repo, opts, params, msg_fmt): """ INTERNAL: Update TARGET_VERSION in params. """ diff --git a/infocalypse/config.py b/infocalypse/config.py --- a/infocalypse/config.py +++ b/infocalypse/config.py @@ -29,6 +29,29 @@ from fcpclient import get_usk_hash, is_u from knownrepos import DEFAULT_TRUST, DEFAULT_GROUPS, \ DEFAULT_NOTIFICATION_GROUP from mercurial import util + +# Similar hack is used in fms.py. +import knownrepos # Just need a module to read __file__ from + +try: + #raise ImportError('fake error to test code path') + __import__('ConfigParser') +except ImportError, err: + # ConfigParser doesn't ship with, the 1.3 Windows binary distro + # http://mercurial.berkwood.com/binaries/Mercurial-1.3.exe + # so we do some hacks to use a local copy. + #print + #print "No ConfigParser? This doesn't look good." + PARTS = os.path.split(os.path.dirname(knownrepos.__file__)) + if PARTS[-1] != 'infocalypse': + print "ConfigParser is missing and couldn't hack path. Giving up. :-(" + else: + PATH = os.path.join(PARTS[0], 'python2_5_files') + sys.path.append(PATH) + #print ("Put local copies of python2.5 ConfigParser.py, " + # + "nntplib.py and netrc.py in path...") + print + from ConfigParser import ConfigParser if sys.platform == 'win32': @@ -328,3 +351,53 @@ class Config: parser.write(out_file) finally: out_file.close() + +# HACK: This really belongs in sitecmds.py but I wanted +# all ConfigParser dependencies in one file because of +# the ConfigParser import hack. See top of file. +def read_freesite_cfg(ui_, repo, params, stored_cfg): + """ Read param out of the freesite.cfg file. """ + cfg_file = os.path.join(repo.root, 'freesite.cfg') + + ui_.status('Using config file:\n%s\n' % cfg_file) + if not os.path.exists(cfg_file): + ui_.warn("Can't read: %s\n" % cfg_file) + raise util.Abort("Use --createconfig to create freesite.cfg") + + parser = ConfigParser() + parser.read(cfg_file) + if not parser.has_section('default'): + raise util.Abort("Can't read default section of config file?") + + params['SITE_NAME'] = parser.get('default', 'site_name') + params['SITE_DIR'] = parser.get('default', 'site_dir') + if parser.has_option('default','default_file'): + params['SITE_DEFAULT_FILE'] = parser.get('default', 'default_file') + else: + params['SITE_DEFAULT_FILE'] = 'index.html' + + if params.get('SITE_KEY'): + return # key set on command line + + if not parser.has_option('default','site_key_file'): + params['SITE_KEY'] = '' + return # Will use the insert SSK for the repo. + + key_file = parser.get('default', 'site_key_file', 'default') + if key_file == 'default': + ui_.status('Using repo insert key as site key.\n') + params['SITE_KEY'] = 'default' + return # Use the insert SSK for the repo. + try: + # Read private key from specified key file relative + # to the directory the .infocalypse config file is stored in. + key_file = os.path.join(os.path.dirname(stored_cfg.file_name), + key_file) + ui_.status('Reading site key from:\n%s\n' % key_file) + params['SITE_KEY'] = open(key_file, 'rb').read().strip() + except IOError: + raise util.Abort("Couldn't read site key from: %s" % key_file) + + if not params['SITE_KEY'].startswith('SSK@'): + raise util.Abort("Stored site key not an SSK?") + diff --git a/infocalypse/devnotes.txt b/infocalypse/devnotes.txt --- a/infocalypse/devnotes.txt +++ b/infocalypse/devnotes.txt @@ -1,3 +1,21 @@ +djk20090714 +Added Python 2.5.4 version of ConfigParser.py to the python2_5_file +directory. This was required to make the berkwood 1.3 Windows +binary distribution of Mercurial work. i.e. here: + +http://mercurial.berkwood.com/binaries/Mercurial-1.3.exe + +Smoke tested on Windows XP w/ 1.2.1, 1.3 binary +Mercurial distros: +http://mercurial.berkwood.com/binaries/Mercurial-1.3.exe +http://mercurial.berkwood.com/binaries/Mercurial-1.2.1.exe + +Looks good. + +djk20090713 +Smoke tested on Ubuntu Jaunty Jackalope with +Mercurial 1.1.2. Looks good. + djk20090706 Added doc/infocalypse_howto.html @@ -13,7 +31,8 @@ Successfully pulled fred_staging/82/ on djk20090702 Minimum required mercurial version is (and probably has been for a -while) 1.2.1. +while) 1.2.1. [djk20090713, Too conservative. 1.1.2 works on +Jaunty Jackalope. 1.0.2 definitely doesn't work anywhere (problems pushing)] That's the only rev. I've been testing with. @@ -42,7 +61,7 @@ I added fn-fmsread and fn-fmsnotify to s receiving repo update notifications via fms. They are documented in __init__.py. -The gensig.py script can be use to publish repo updates +The gensig.py script can be used to publish repo updates in your fms signature. IMPORTANT: diff --git a/infocalypse/fms.py b/infocalypse/fms.py --- a/infocalypse/fms.py +++ b/infocalypse/fms.py @@ -29,6 +29,7 @@ from fcpclient import get_usk_hash, get_ # Hmmm... This dependency doesn't really belong here. from knownrepos import KNOWN_REPOS +# Similar HACK is used in config.py import knownrepos # Just need a module to read __file__ from try: @@ -46,8 +47,8 @@ except ImportError, err: else: PATH = os.path.join(PARTS[0], 'python2_5_files') sys.path.append(PATH) - # Seems to work ok with 2.6... - #print "Put local copies of python2.5 nntplib.py and netrc.py in path..." + #print ("Put local copies of python2.5 ConfigParser.py, " + # + "nntplib.py and netrc.py in path...") #print # REDFLAG: Research. # Can't catch ImportError? Always aborts. ??? diff --git a/infocalypse/sitecmds.py b/infocalypse/sitecmds.py --- a/infocalypse/sitecmds.py +++ b/infocalypse/sitecmds.py @@ -22,8 +22,6 @@ import os -from ConfigParser import ConfigParser - from mercurial import util from fcpconnection import FCPError @@ -62,52 +60,6 @@ site_dir = site_root ui_.status('Created config file:\n%s\n' % file_name) ui_.status('You probably want to edit at least the site_name.\n') -def read_freesite_cfg(ui_, repo, params, stored_cfg): - """ Read param out of the freesite.cfg file. """ - cfg_file = os.path.join(repo.root, 'freesite.cfg') - - ui_.status('Using config file:\n%s\n' % cfg_file) - if not os.path.exists(cfg_file): - ui_.warn("Can't read: %s\n" % cfg_file) - raise util.Abort("Use --createconfig to create freesite.cfg") - - parser = ConfigParser() - parser.read(cfg_file) - if not parser.has_section('default'): - raise util.Abort("Can't read default section of config file?") - - params['SITE_NAME'] = parser.get('default', 'site_name') - params['SITE_DIR'] = parser.get('default', 'site_dir') - if parser.has_option('default','default_file'): - params['SITE_DEFAULT_FILE'] = parser.get('default', 'default_file') - else: - params['SITE_DEFAULT_FILE'] = 'index.html' - - if params.get('SITE_KEY'): - return # key set on command line - - if not parser.has_option('default','site_key_file'): - params['SITE_KEY'] = '' - return # Will use the insert SSK for the repo. - - key_file = parser.get('default', 'site_key_file', 'default') - if key_file == 'default': - ui_.status('Using repo insert key as site key.\n') - params['SITE_KEY'] = 'default' - return # Use the insert SSK for the repo. - try: - # Read private key from specified key file relative - # to the directory the .infocalypse config file is stored in. - key_file = os.path.join(os.path.dirname(stored_cfg.file_name), - key_file) - ui_.status('Reading site key from:\n%s\n' % key_file) - params['SITE_KEY'] = open(key_file, 'rb').read().strip() - except IOError: - raise util.Abort("Couldn't read site key from: %s" % key_file) - - if not params['SITE_KEY'].startswith('SSK@'): - raise util.Abort("Stored site key not an SSK?") - def get_insert_uri(params): """ Helper function builds the insert URI. """ if params['SITE_KEY'] == 'CHK@': diff --git a/python2_5_files/ConfigParser.py b/python2_5_files/ConfigParser.py new file mode 100644 --- /dev/null +++ b/python2_5_files/ConfigParser.py @@ -0,0 +1,641 @@ +# Taken from Python 2.5.4 +"""Configuration file parser. + +A setup file consists of sections, lead by a "[section]" header, +and followed by "name: value" entries, with continuations and such in +the style of RFC 822. + +The option values can contain format strings which refer to other values in +the same section, or values in a special [DEFAULT] section. + +For example: + + something: %(dir)s/whatever + +would resolve the "%(dir)s" to the value of dir. All reference +expansions are done late, on demand. + +Intrinsic defaults can be specified by passing them into the +ConfigParser constructor as a dictionary. + +class: + +ConfigParser -- responsible for parsing a list of + configuration files, and managing the parsed database. + + methods: + + __init__(defaults=None) + create the parser and specify a dictionary of intrinsic defaults. The + keys must be strings, the values must be appropriate for %()s string + interpolation. Note that `__name__' is always an intrinsic default; + its value is the section's name. + + sections() + return all the configuration section names, sans DEFAULT + + has_section(section) + return whether the given section exists + + has_option(section, option) + return whether the given option exists in the given section + + options(section) + return list of configuration options for the named section + + read(filenames) + read and parse the list of named configuration files, given by + name. A single filename is also allowed. Non-existing files + are ignored. Return list of successfully read files. + + readfp(fp, filename=None) + read and parse one configuration file, given as a file object. + The filename defaults to fp.name; it is only used in error + messages (if fp has no `name' attribute, the string `<???>' is used). + + get(section, option, raw=False, vars=None) + return a string value for the named option. All % interpolations are + expanded in the return values, based on the defaults passed into the + constructor and the DEFAULT section. Additional substitutions may be + provided using the `vars' argument, which must be a dictionary whose + contents override any pre-existing defaults. + + getint(section, options) + like get(), but convert value to an integer + + getfloat(section, options) + like get(), but convert value to a float + + getboolean(section, options) + like get(), but convert value to a boolean (currently case + insensitively defined as 0, false, no, off for False, and 1, true, + yes, on for True). Returns False or True. + + items(section, raw=False, vars=None) + return a list of tuples with (name, value) for each option + in the section. + + remove_section(section) + remove the given file section and all its options + + remove_option(section, option) + remove the given option from the given section + + set(section, option, value) + set the given option + + write(fp) + write the configuration state in .ini format +""" + +import re + +__all__ = ["NoSectionError", "DuplicateSectionError", "NoOptionError", + "InterpolationError", "InterpolationDepthError", + "InterpolationSyntaxError", "ParsingError", + "MissingSectionHeaderError", + "ConfigParser", "SafeConfigParser", "RawConfigParser", + "DEFAULTSECT", "MAX_INTERPOLATION_DEPTH"] + +DEFAULTSECT = "DEFAULT" + +MAX_INTERPOLATION_DEPTH = 10 + + + +# exception classes +class Error(Exception): + """Base class for ConfigParser exceptions.""" + + def __init__(self, msg=''): + self.message = msg + Exception.__init__(self, msg) + + def __repr__(self): + return self.message + + __str__ = __repr__ + +class NoSectionError(Error): + """Raised when no section matches a requested option.""" + + def __init__(self, section): + Error.__init__(self, 'No section: %r' % (section,)) + self.section = section + +class DuplicateSectionError(Error): + """Raised when a section is multiply-created.""" + + def __init__(self, section): + Error.__init__(self, "Section %r already exists" % section) + self.section = section + +class NoOptionError(Error): + """A requested option was not found.""" + + def __init__(self, option, section): + Error.__init__(self, "No option %r in section: %r" % + (option, section)) + self.option = option + self.section = section + +class InterpolationError(Error): + """Base class for interpolation-related exceptions.""" + + def __init__(self, option, section, msg): + Error.__init__(self, msg) + self.option = option + self.section = section + +class InterpolationMissingOptionError(InterpolationError): + """A string substitution required a setting which was not available.""" + + def __init__(self, option, section, rawval, reference): + msg = ("Bad value substitution:\n" + "\tsection: [%s]\n" + "\toption : %s\n" + "\tkey : %s\n" + "\trawval : %s\n" + % (section, option, reference, rawval)) + InterpolationError.__init__(self, option, section, msg) + self.reference = reference + +class InterpolationSyntaxError(InterpolationError): + """Raised when the source text into which substitutions are made + does not conform to the required syntax.""" + +class InterpolationDepthError(InterpolationError): + """Raised when substitutions are nested too deeply.""" + + def __init__(self, option, section, rawval): + msg = ("Value interpolation too deeply recursive:\n" + "\tsection: [%s]\n" + "\toption : %s\n" + "\trawval : %s\n" + % (section, option, rawval)) + InterpolationError.__init__(self, option, section, msg) + +class ParsingError(Error): + """Raised when a configuration file does not follow legal syntax.""" + + def __init__(self, filename): + Error.__init__(self, 'File contains parsing errors: %s' % filename) + self.filename = filename + self.errors = [] + + def append(self, lineno, line): + self.errors.append((lineno, line)) + self.message += '\n\t[line %2d]: %s' % (lineno, line) + +class MissingSectionHeaderError(ParsingError): + """Raised when a key-value pair is found before any section header.""" + + def __init__(self, filename, lineno, line): + Error.__init__( + self, + 'File contains no section headers.\nfile: %s, line: %d\n%r' % + (filename, lineno, line)) + self.filename = filename + self.lineno = lineno + self.line = line + + + +class RawConfigParser: + def __init__(self, defaults=None): + self._sections = {} + self._defaults = {} + if defaults: + for key, value in defaults.items(): + self._defaults[self.optionxform(key)] = value + + def defaults(self): + return self._defaults + + def sections(self): + """Return a list of section names, excluding [DEFAULT]""" + # self._sections will never have [DEFAULT] in it + return self._sections.keys() + + def add_section(self, section): + """Create a new section in the configuration. + + Raise DuplicateSectionError if a section by the specified name + already exists. + """ + if section in self._sections: + raise DuplicateSectionError(section) + self._sections[section] = {} + + def has_section(self, section): + """Indicate whether the named section is present in the configuration. + + The DEFAULT section is not acknowledged. + """ + return section in self._sections + + def options(self, section): + """Return a list of option names for the given section name.""" + try: + opts = self._sections[section].copy() + except KeyError: + raise NoSectionError(section) + opts.update(self._defaults) + if '__name__' in opts: + del opts['__name__'] + return opts.keys() + + def read(self, filenames): + """Read and parse a filename or a list of filenames. + + Files that cannot be opened are silently ignored; this is + designed so that you can specify a list of potential + configuration file locations (e.g. current directory, user's + home directory, systemwide directory), and all existing + configuration files in the list will be read. A single + filename may also be given. + + Return list of successfully read files. + """ + if isinstance(filenames, basestring): + filenames = [filenames] + read_ok = [] + for filename in filenames: + try: + fp = open(filename) + except IOError: + continue + self._read(fp, filename) + fp.close() + read_ok.append(filename) + return read_ok + + def readfp(self, fp, filename=None): + """Like read() but the argument must be a file-like object. + + The `fp' argument must have a `readline' method. Optional + second argument is the `filename', which if not given, is + taken from fp.name. If fp has no `name' attribute, `<???>' is + used. + + """ + if filename is None: + try: + filename = fp.name + except AttributeError: + filename = '<???>' + self._read(fp, filename) + + def get(self, section, option): + opt = self.optionxform(option) + if section not in self._sections: + if section != DEFAULTSECT: + raise NoSectionError(section) + if opt in self._defaults: + return self._defaults[opt] + else: + raise NoOptionError(option, section) + elif opt in self._sections[section]: + return self._sections[section][opt] + elif opt in self._defaults: + return self._defaults[opt] + else: + raise NoOptionError(option, section) + + def items(self, section): + try: + d2 = self._sections[section] + except KeyError: + if section != DEFAULTSECT: + raise NoSectionError(section) + d2 = {} + d = self._defaults.copy() + d.update(d2) + if "__name__" in d: + del d["__name__"] + return d.items() + + def _get(self, section, conv, option): + return conv(self.get(section, option)) + + def getint(self, section, option): + return self._get(section, int, option) + + def getfloat(self, section, option): + return self._get(section, float, option) + + _boolean_states = {'1': True, 'yes': True, 'true': True, 'on': True, + '0': False, 'no': False, 'false': False, 'off': False} + + def getboolean(self, section, option): + v = self.get(section, option) + if v.lower() not in self._boolean_states: + raise ValueError, 'Not a boolean: %s' % v + return self._boolean_states[v.lower()] + + def optionxform(self, optionstr): + return optionstr.lower() + + def has_option(self, section, option): + """Check for the existence of a given option in a given section.""" + if not section or section == DEFAULTSECT: + option = self.optionxform(option) + return option in self._defaults + elif section not in self._sections: + return False + else: + option = self.optionxform(option) + return (option in self._sections[section] + or option in self._defaults) + + def set(self, section, option, value): + """Set an option.""" + if not section or section == DEFAULTSECT: + sectdict = self._defaults + else: + try: + sectdict = self._sections[section] + except KeyError: + raise NoSectionError(section) + sectdict[self.optionxform(option)] = value + + def write(self, fp): + """Write an .ini-format representation of the configuration state.""" + if self._defaults: + fp.write("[%s]\n" % DEFAULTSECT) + for (key, value) in self._defaults.items(): + fp.write("%s = %s\n" % (key, str(value).replace('\n', '\n\t'))) + fp.write("\n") + for section in self._sections: + fp.write("[%s]\n" % section) + for (key, value) in self._sections[section].items(): + if key != "__name__": + fp.write("%s = %s\n" % + (key, str(value).replace('\n', '\n\t'))) + fp.write("\n") + + def remove_option(self, section, option): + """Remove an option.""" + if not section or section == DEFAULTSECT: + sectdict = self._defaults + else: + try: + sectdict = self._sections[section] + except KeyError: + raise NoSectionError(section) + option = self.optionxform(option) + existed = option in sectdict + if existed: + del sectdict[option] + return existed + + def remove_section(self, section): + """Remove a file section.""" + existed = section in self._sections + if existed: + del self._sections[section] + return existed + + # + # Regular expressions for parsing section headers and options. + # + 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 + ) + + def _read(self, fp, fpname): + """Parse a sectioned setup file. + + The sections in setup file contains a title line at the top, + indicated by a name in square brackets (`[]'), plus key/value + options lines, indicated by `name: value' format lines. + Continuations are represented by an embedded newline then + leading whitespace. Blank lines, lines beginning with a '#', + and just about everything else are ignored. + """ + cursect = None # None, or a dictionary + optname = None + lineno = 0 + e = None # None, or an exception + while True: + line = fp.readline() + if not line: + break + lineno = lineno + 1 + # comment or blank line? + if line.strip() == '' or line[0] in '#;': + continue + if line.split(None, 1)[0].lower() == 'rem' and line[0] in "rR": + # no leading whitespace + continue + # continuation line? + if line[0].isspace() and cursect is not None and optname: + value = line.strip() + if value: + cursect[optname] = "%s\n%s" % (cursect[optname], value) + # a section header or option header? + else: + # is it a section header? + mo = self.SECTCRE.match(line) + if mo: + sectname = mo.group('header') + if sectname in self._sections: + cursect = self._sections[sectname] + elif sectname == DEFAULTSECT: + cursect = self._defaults + else: + cursect = {'__name__': sectname} + self._sections[sectname] = cursect + # So sections can't start with a continuation line + optname = None + # no section header in the file? + elif cursect is None: + raise MissingSectionHeaderError(fpname, lineno, line) + # an option line? + else: + mo = self.OPTCRE.match(line) + if mo: + optname, vi, optval = mo.group('option', 'vi', 'value') + if vi in ('=', ':') and ';' in optval: + # ';' is a comment delimiter only if it follows + # a spacing character + pos = optval.find(';') + if pos != -1 and optval[pos-1].isspace(): + optval = optval[:pos] + optval = optval.strip() + # allow empty values + if optval == '""': + optval = '' + optname = self.optionxform(optname.rstrip()) + cursect[optname] = optval + else: + # a non-fatal parsing error occurred. set up the + # exception but keep going. the exception will be + # raised at the end of the file and will contain a + # list of all bogus lines + if not e: + e = ParsingError(fpname) + e.append(lineno, repr(line)) + # if any parsing errors occurred, raise an exception + if e: + raise e + + +class ConfigParser(RawConfigParser): + + def get(self, section, option, raw=False, vars=None): + """Get an option value for a given section. + + All % interpolations are expanded in the return values, based on the + defaults passed into the constructor, unless the optional argument + `raw' is true. Additional substitutions may be provided using the + `vars' argument, which must be a dictionary whose contents overrides + any pre-existing defaults. + + The section DEFAULT is special. + """ + d = self._defaults.copy() + try: + d.update(self._sections[section]) + except KeyError: + if section != DEFAULTSECT: + raise NoSectionError(section) + # Update with the entry specific variables + if vars: + for key, value in vars.items(): + d[self.optionxform(key)] = value + option = self.optionxform(option) + try: + value = d[option] + except KeyError: + raise NoOptionError(option, section) + + if raw: + return value + else: + return self._interpolate(section, option, value, d) + + def items(self, section, raw=False, vars=None): + """Return a list of tuples with (name, value) for each option + in the section. + + All % interpolations are expanded in the return values, based on the + defaults passed into the constructor, unless the optional argument + `raw' is true. Additional substitutions may be provided using the + `vars' argument, which must be a dictionary whose contents overrides + any pre-existing defaults. + + The section DEFAULT is special. + """ + d = self._defaults.copy() + try: + d.update(self._sections[section]) + except KeyError: + if section != DEFAULTSECT: + raise NoSectionError(section) + # Update with the entry specific variables + if vars: + for key, value in vars.items(): + d[self.optionxform(key)] = value + options = d.keys() + if "__name__" in options: + options.remove("__name__") + if raw: + return [(option, d[option]) + for option in options] + else: + return [(option, self._interpolate(section, option, d[option], d)) + for option in options] + + def _interpolate(self, section, option, rawval, vars): + # do the string interpolation + value = rawval + depth = MAX_INTERPOLATION_DEPTH + while depth: # Loop through this until it's done + depth -= 1 + if "%(" in value: + value = self._KEYCRE.sub(self._interpolation_replace, value) + try: + value = value % vars + except KeyError, e: + raise InterpolationMissingOptionError( + option, section, rawval, e[0]) + else: + break + if "%(" in value: + raise InterpolationDepthError(option, section, rawval) + return value + + _KEYCRE = re.compile(r"%\(([^)]*)\)s|.") + + def _interpolation_replace(self, match): + s = match.group(1) + if s is None: + return match.group() + else: + return "%%(%s)s" % self.optionxform(s) + + +class SafeConfigParser(ConfigParser): + + def _interpolate(self, section, option, rawval, vars): + # do the string interpolation + L = [] + self._interpolate_some(option, L, rawval, section, vars, 1) + return ''.join(L) + + _interpvar_match = re.compile(r"%\(([^)]+)\)s").match + + def _interpolate_some(self, option, accum, rest, section, map, depth): + if depth > MAX_INTERPOLATION_DEPTH: + raise InterpolationDepthError(option, section, rest) + while rest: + p = rest.find("%") + if p < 0: + accum.append(rest) + return + if p > 0: + accum.append(rest[:p]) + rest = rest[p:] + # p is no longer used + c = rest[1:2] + if c == "%": + accum.append("%") + rest = rest[2:] + elif c == "(": + m = self._interpvar_match(rest) + if m is None: + raise InterpolationSyntaxError(option, section, + "bad interpolation variable reference %r" % rest) + var = self.optionxform(m.group(1)) + rest = rest[m.end():] + try: + v = map[var] + except KeyError: + raise InterpolationMissingOptionError( + option, section, rest, var) + if "%" in v: + self._interpolate_some(option, accum, v, + section, map, depth + 1) + else: + accum.append(v) + else: + raise InterpolationSyntaxError( + option, section, + "'%%' must be followed by '%%' or '(', found: %r" % (rest,)) + + def set(self, section, option, value): + """Set an option. Extend ConfigParser.set: check for string values.""" + if not isinstance(value, basestring): + raise TypeError("option values must be strings") + ConfigParser.set(self, section, option, value) diff --git a/python2_5_files/netrc.py b/python2_5_files/netrc.py --- a/python2_5_files/netrc.py +++ b/python2_5_files/netrc.py @@ -1,3 +1,4 @@ +# Taken from Python 2.5.4 """An object-oriented interface to .netrc files.""" # Module and documentation by Eric S. Raymond, 21 Dec 1998 diff --git a/python2_5_files/nntplib.py b/python2_5_files/nntplib.py --- a/python2_5_files/nntplib.py +++ b/python2_5_files/nntplib.py @@ -1,3 +1,4 @@ +# Taken from Python 2.5.4 """An NNTP client class based on RFC 977: Network News Transfer Protocol. Example: