# -*- coding: utf-8 -*-
r"""
A wrapper around configparser that defines syntax and types within
the configuration options.
This tries to do for configuration processing what optparse did for
command line option processing: A declarative way of handling the main
chores.
The idea is that, in a client program, you say something like::
from pftf.fancyconfig import (Configuration, Section, ConfigError,
...(items you want)...)
_config = Config(
Section(...
XYConfigItem(...)
),
Section(...
...
)
)
get = _config.get
set = _config.set
if __name__=="__main__":
print fancyconfig.makeTxtDocs(_config)
else:
try:
fancyconfig.readConfiguration(_config, None,
os.path.join(dataDir, "config"))
except ConfigError, msg:
import sys
sys.stderr.write("%s: %s\n"%(
sys.argv[0], unicode(msg)))
sys.exit(0)
and be done with most of it.
For examples of how this is used, see pftf (http://www.tfiu.de/pftf)
or DaCHS (http://soft.g-vo.org/dachs)
"""
#c Copyright 2008-2023, the GAVO project <gavo@ari.uni-heidelberg.de>
#c
#c This program is free software, covered by the GNU GPL. See the
#c COPYING file in the source distribution.
import codecs
import configparser
import re
import os
import tempfile
import warnings
import weakref
defaultSection = "general" # must be all lowercase
# Set to true if avoid making a bad config item bomb out
BAD_CONFIG_ITEM_JUST_WARNS = True
[docs]class ConfigError(Exception):
"""is the base class of the user visible exceptions from this module.
"""
fileName = "<internal>"
[docs]class ParseError(ConfigError):
"""is raised by ConfigItem's parse methods if there is a problem with
the input.
These should only escape to users of this module unless they call
ConfigItem.set themselves (which they shouldn't).
"""
[docs]class NoConfigItem(ConfigError):
"""is raised by Configuration if a non-existing configuration
item is set or requested.
"""
[docs]class BadConfigValue(ConfigError):
"""is raised by getConfiguration when there is a syntax error or the
like in a value.
The error message gives a hint at the reason of the error and is intended
for human consumption.
"""
[docs]class SyntaxError(ConfigError):
"""is raised when the input file syntax is bad (i.e., on
configparser.ParsingErrors)
"""
[docs]class ConfigItem(object):
"""A description of a configuration item including methods
to parse and unparse them.
This class is an abstract base class for options with real syntax
(_parse and _unparse methods).
ConfigItems have a section and a name (as in configparser), a
value (that defaults to default), an origin (which is "default",
if the value has not been changed and otherwise can be freely
used by clients), and a description. The origin is important
for distinguishing what to save.
You need to define the _parse and _unparse methods when deriving
subclasses. The _parse methods must take a string and return anything or
raise ParseErrors (with
a sensible description of the problem) if there is a problem
with the input; they must not raise other exceptions when passed a
string (but may do anything when passed something else). _unparse
methods must not raise exceptions, take a value as returned
by parse (nothing else must be passed in) and return a
string that _parse would parse into this value.
Thus, the set method *only* takes strings as values. To set
parsed values, assign to the value attribute directly. However, _unparse
methods are not required to cope with any crazy stuff you enter
in this way, and thus you suddenly may receive all kinds of
funny exceptions when serializing a Configuration.
Inheriting classes need to specify a class attribute default that
kicks in when no default has been specified during construction.
These must be strings parseable by _parse.
Finally, you should provide a typedesc class attribute, a description
of the type intended for human consumption. See the documentation
functions below to get an idea how the would be shown.
"""
typedesc = "unspecified value"
def __init__(self, name, default=None, description="Undocumented"):
self.name = name
if default is None:
default = self.default
self.default = default
self.set(default, "default")
self.description = description
self.parent = None # will be set on adoption by a Configuration
[docs] def set(self, value, origin="user"):
self.value, self.origin = self._parse(value), origin
[docs] def getAsString(self):
return self._unparse(self.value)
def _parse(self, value):
raise ParseError("Internal error: Base config item used.")
def _unparse(self, value):
return value
[docs]class StringConfigItem(ConfigItem):
"""A config item containing unicode strings.
The serialization of the config file is supposed to be utf-8.
The special value None is used as a Null value literal.
Tests are below.
"""
typedesc = "string"
default = ""
def _parse(self, value):
if value=="None":
return None
return value
def _unparse(self, value):
if value is None:
return "None"
return value
[docs]class BytestringConfigItem(ConfigItem):
"""A config item containing byte strings. No characters outside
of ASCII are allowed.
"""
typedesc = "ASCII string"
default = ""
def _parse(self, value):
if value=="None":
return None
return str(value)
def _unparse(self, value):
return str(value)
[docs]class IntConfigItem(ConfigItem):
"""A config item containing an integer.
It supports a Null value through the special None literal.
>>> ci = IntConfigItem("foo"); print(ci.value)
None
>>> ci = IntConfigItem("foo", default="23"); ci.value
23
>>> ci.set("42"); ci.value
42
>>> ci.getAsString()
'42'
"""
typedesc = "integer"
default = "None"
def _parse(self, value):
if value=="None":
return None
try:
return int(value)
except ValueError:
raise ParseError(f"{value} is not an integer literal"
)
def _unparse(self, value):
return str(value)
[docs]class FloatConfigItem(ConfigItem):
"""A config item containing a float.
It supports a Null value through the special None literal.
>>> ci = FloatConfigItem("foo"); print(ci.value)
None
>>> ci = FloatConfigItem("foo", default="23"); ci.value
23.0
>>> ci.set("42.25"); ci.value
42.25
>>> ci.getAsString()
'42.25'
"""
typedesc = "floating point value"
default = "None"
def _parse(self, value):
if value=="None":
return None
try:
return float(value)
except ValueError:
raise ParseError("%s is not a floating point literal"%value
)
def _unparse(self, value):
return repr(value)
[docs]class ListConfigItem(StringConfigItem):
r"""A ConfigItem containing a list of strings, comma separated.
The values are space-normalized. Trailing whitespace-only items are
discarded, so "" is an empty list, "," is a list containing one
empty string.
There is currently no way to embed commas in the values. If that
should become necessary, I'd probably go for backslash escaping.
>>> ci = ListConfigItem("foo"); ci.value, ci.getAsString()
([], '')
>>> ci.set(ci.getAsString());ci.value
[]
>>> ci.set("3, 2, 1, Zündung"); ci.value, ci.getAsString()
(['3', '2', '1', 'Zündung'], '3, 2, 1, Zündung, ')
>>> ci.set(",");ci.value
['']
>>> ci.set(ci.getAsString());ci.value
['']
"""
typedesc = "list of strings"
default = ""
def _parse(self, value):
res = [s.strip()
for s in StringConfigItem._parse(self, value).split(",")]
if not res[-1]:
del res[-1]
return res
def _unparse(self, value):
return StringConfigItem._unparse(self, ", ".join(value+[""]))
[docs]class SetConfigItem(ListConfigItem):
"""A set-valued ListConfigItem for quick existence lookups.
"""
typedesc = "set of strings"
def _parse(self, value):
return set(ListConfigItem._parse(self, value))
def _unparse(self, value):
return StringConfigItem._unparse(self, ", ".join(value))
[docs]class IntListConfigItem(ListConfigItem):
"""A ConfigItem containing a comma separated list of ints.
Literal handling is analoguos to ListConfigItem.
>>> ci = IntListConfigItem("foo"); ci.value, ci.getAsString()
([], '')
>>> ci.set("3,2, 1"); ci.value, ci.getAsString()
([3, 2, 1], '3, 2, 1, ')
>>> ci.set(ci.getAsString()); ci.value
[3, 2, 1]
>>> ci.set("1, 2, 3, rubbish")
Traceback (most recent call last):
...
fancyconfig.ParseError: Non-integer in integer list
"""
typedesc = "list of integers"
default = ""
def _parse(self, value):
try:
return [int(s) for s in ListConfigItem._parse(self, value)]
except ValueError:
raise ParseError("Non-integer in integer list") from None
def _unparse(self, value):
return ListConfigItem._unparse(self, [str(n) for n in value])
[docs]class IntSetConfigItem(IntListConfigItem):
"""A set-valued IntListConfigItem for fast existence lookups.
"""
typedesc = "set of integers"
def _parse(self, value):
return set(IntListConfigItem._parse(self, value))
[docs]class DictConfigItem(ListConfigItem):
r"""A config item that contains a concise representation of
a string-string mapping.
The literal format is {<key>:<value>,}, where whitespace is ignored
between tokens and the last comma may be omitted.
No commas and colons are allowed within keys and values. To lift this,
I'd probably go for backslash escaping.
>>> ci = DictConfigItem("foo"); ci.value
{}
>>> ci.set("ab:cd, foo:Fuß"); ci.value
{'ab': 'cd', 'foo': 'Fuß'}
>>> ci.getAsString();ci.set(ci.getAsString()); ci.value
'ab:cd, foo:Fuß, '
{'ab': 'cd', 'foo': 'Fuß'}
>>> ci.set("ab:cd, rubbish")
Traceback (most recent call last):
...
fancyconfig.ParseError: 'rubbish' is not a valid mapping literal element
"""
typedesc = "mapping"
default = ""
def _parse(self, value):
res = {}
for item in ListConfigItem._parse(self, value):
try:
k, v = item.split(":")
res[k.strip()] = v.strip()
except ValueError:
raise ParseError(f"'{item}' is not a valid mapping literal element"
) from None
return res
def _unparse(self, value):
return ListConfigItem._unparse(self,
["%s:%s"%(k, v) for k, v in value.items()])
[docs]class BooleanConfigItem(ConfigItem):
"""A config item that contains a boolean and can be parsed from
many fancy representations.
"""
typedesc = "boolean"
default = "False"
trueLiterals = set(["true", "yes", "t", "on", "enabled", "1"])
falseLiterals = set(["false", "no", "f", "off", "disabled", "0"])
def _parse(self, value):
value = value.lower()
if value in self.trueLiterals:
return True
elif value in self.falseLiterals:
return False
else:
raise ParseError(
f"'{value}' is no recognized boolean literal.")
def _unparse(self, value):
return {True: "True", False: "False"}[value]
[docs]class EnumeratedConfigItem(StringConfigItem):
"""A ConfigItem taking string values out of a set of possible strings.
Use the keyword argument options to pass in the possible strings.
The first item becomes the default unless you give a default.
You must give a non-empty list of strings as options.
"""
typedesc = "value from a defined set"
def __init__(self, name, default=None, description="Undocumented",
options=[]):
if default is None:
default = options[0]
self.options = set(options)
self.typedesc = "value from the list %s"%(", ".join(
sorted(self.options)))
StringConfigItem.__init__(self, name, default, description)
def _parse(self, value):
encVal = StringConfigItem._parse(self, value)
if encVal not in self.options:
raise ParseError("%s is not an allowed value. Choose one of"
" %s"%(value, ", ".join([o for o in self.options])))
return encVal
[docs]class PathConfigItem(StringConfigItem):
"""A ConfigItem for a unix shell-type path.
The individual items are separated by colons, ~ is replaced by the
current value of $HOME (or "/", if unset), and $<key> substitutions
are supported, with key having to point to a key in the defaultSection.
To embed a real $ sign, double it.
This is parented ConfigItem, i.e., it needs a Configuration parent
before its value can be accessed.
"""
typedesc = "shell-type path"
def _parse(self, value):
self._unparsed = StringConfigItem._parse(self, value)
if self._unparsed is None:
return []
else:
return [s.strip() for s in self._unparsed.split(":")]
def _unparse(self, value):
return StringConfigItem._unparse(self, self._unparsed)
def _getValue(self):
def resolveReference(mat):
if mat.group(1)=='$':
return '$'
else:
return self.parent.get(mat.group(1))
res = []
for p in self._value:
if p.startswith("~"):
p = os.environ.get("HOME", "")+p[1:]
if '$' in p:
p = re.sub(r"\$(\w+)", resolveReference, p)
res.append(p)
return res
def _setValue(self, val):
self._value = val
value = property(_getValue, _setValue)
[docs]class PathRelativeConfigItem(StringConfigItem):
"""A configuration item interpreted relative to a path
given in the general section.
Basically, this is a replacement for configparser's %(x)s interpolation.
In addition, we expand ~ in front of a value to the current value of
$HOME.
To enable general-type interpolation, override the baseKey class Attribute.
"""
baseKey = None
_value = ""
def _getValue(self):
if self._value is None:
return None
if self._value.startswith("~"):
return os.environ.get("HOME", "/no_home")+self._value[1:]
if self.baseKey:
return os.path.join(self.parent.get(self.baseKey), self._value)
return self._value
def _setValue(self, val):
self._value = val
value = property(_getValue, _setValue)
[docs]class ExpandedPathConfigItem(StringConfigItem):
"""A configuration item in that returns its value expandusered.
"""
def _parse(self, value):
val = StringConfigItem._parse(self, value)
if val is not None:
val = os.path.expanduser(val)
return val
class _Undefined(object):
"""A sentinel for section.get.
"""
[docs]class Section(object):
"""A section within the configuration.
It is constructed with a name, a documentation, and the configuration
items.
They double as proxies between the configuration and their items
via the setParent method.
"""
def __init__(self, name, documentation, *items):
self.name, self.documentation = name, documentation
self.items = {}
for item in items:
self.items[item.name.lower()] = item
def __iter__(self):
for name in sorted(self.items):
yield self.items[name]
[docs] def getitem(self, name):
if name.lower() in self.items:
return self.items[name.lower()]
else:
raise NoConfigItem("No such configuration item: [%s] %s"%(
self.name, name))
[docs] def get(self, name):
"""returns the value of the configuration item name.
If it does not exist, a NoConfigItem exception will be raised.
"""
return self.getitem(name).value
[docs] def set(self, name, value, origin="user"):
"""set the value of the configuration item name.
value must always be a string, regardless of the item's actual type.
"""
try:
self.getitem(name).set(value, origin)
except NoConfigItem:
if BAD_CONFIG_ITEM_JUST_WARNS:
warnings.warn("Unknown configuration item [%s] %s ignored."%(
self.name, name))
else:
raise
[docs] def setParent(self, parent):
for item in list(self.items.values()):
item.parent = parent
[docs]class DefaultSection(Section):
"""is the default section, named by defaultSection above.
The only difference to Section is that you leave out the name.
"""
def __init__(self, documentation, *items):
Section.__init__(self, defaultSection, documentation, *items)
[docs]class MagicSection(Section):
"""A section that creates new keys on the fly.
Use this a dictionary-like thing when successive edits are
necessary or the DictConfigItem becomes too unwieldy.
A MagicSection is constructed with the section name, an item
factory, which has to be a subclass of ConfigItem (you may
want to write a special constructor to provide documentation,
etc.), and defaults as a sequence of pairs of keys and values.
And there should be documentation, too, of course.
"""
def __init__(self, name, documentation="Undocumented",
itemFactory=StringConfigItem, defaults=[]):
self.itemFactory = itemFactory
items = []
for key, value in defaults:
items.append(self.itemFactory(key))
items[-1].set(value, origin="defaults")
Section.__init__(self, name, documentation, *items)
[docs] def set(self, name, value, origin="user"):
if name not in self.items:
self.items[name.lower()] = self.itemFactory(name)
Section.set(self, name, value, origin)
[docs]class Configuration(object):
"""A collection of config Sections and provides an interface to access
them and their items.
You construct it with the Sections you want and then use the get
method to access their content. You can either use get(section, name)
or just get(name), which implies the defaultSection section defined
at the top (right now, "general").
To read configuration items, use addFromFp. addFromFp should only
raise subclasses of ConfigError.
You can also set individual items using set.
The class follows the default behaviour of configparser in that section
and item names are lowercased.
Note that direct access to sections is not forbidden, but you have to
keep case mangling of keys into account when doing so.
"""
def __init__(self, *sections):
self.sections = {}
for section in sections:
self.sections[section.name.lower()] = section
section.setParent(weakref.proxy(self))
def __iter__(self):
sectHeads = list(self.sections.keys())
if defaultSection in sectHeads:
sectHeads.remove(defaultSection)
yield self.sections[defaultSection]
for h in sorted(sectHeads):
yield self.sections[h]
[docs] def getitem(self, arg1, arg2=None):
"""returns the *item* described by section, name or just name.
"""
if arg2 is None:
section, name = defaultSection, arg1
else:
section, name = arg1, arg2
if section.lower() in self.sections:
return self.sections[section.lower()].getitem(name)
raise NoConfigItem("No such configuration item: [%s] %s"%(
section, name))
[docs] def get(self, arg1, arg2=None, default=_Undefined):
try:
return self.getitem(arg1, arg2).value
except NoConfigItem:
if default is _Undefined:
raise
return default
[docs] def set(self, arg1, arg2, arg3=None, origin="user"):
"""sets a configuration item to a value.
arg1 can be a section, in which case arg2 is a key and arg3 is a
value; alternatively, if arg3 is not given, arg1 is a key in
the defaultSection, and arg2 is the value.
All arguments are strings that must be parseable by the referenced
item's _parse method.
Origin is a tag you can use to, e.g., determine what to save.
"""
if arg3 is None:
section, name, value = defaultSection, arg1, arg2
else:
section, name, value = arg1, arg2, arg3
if section.lower() in self.sections:
return self.sections[section.lower()].set(name, value, origin)
else:
raise NoConfigItem("No such configuration item: [%s] %s"%(
section, name))
[docs] def addFromFp(self, fp, origin="user", fName="<internal>"):
"""adds the config items in the file fp to self.
"""
p = configparser.ConfigParser()
try:
p.read_file(fp, fName)
except configparser.ParsingError as msg:
raise SyntaxError("Config syntax error in %s: %s"%(fName,
str(msg)))
sections = p.sections()
for section in sections:
for name, value in p.items(section):
try:
self.set(section, name, value, origin)
except ParseError as msg:
raise BadConfigValue("While parsing value of %s in section %s,"
" file %s:\n%s"%
(name, section, fName, str(msg)))
[docs] def getUserConfig(self):
"""returns a configparser containing the user set config items.
"""
userConf = configparser.ConfigParser()
for section in list(self.sections.values()):
for item in section:
if item.origin=="user":
if not userConf.has_section(section.name):
userConf.add_section(section.name)
userConf.set(section.name, item.name, item.getAsString())
return userConf
[docs] def saveUserConfig(self, destName):
"""writes the config items changed by the user to destName.
"""
uc = self.getUserConfig()
fd, tmpName = tempfile.mkstemp("temp", "", dir=os.path.dirname(destName))
f = os.fdopen(fd, "w")
uc.write(f)
f.flush()
os.fsync(fd)
f.close()
os.rename(tmpName, destName)
def _addToConfig(config, fName, origin):
"""adds the config items in the file named in fName to the Configuration,
tagging them with origin.
fName can be None or point to a non-exisiting file. In both cases,
the function does nothing.
"""
if not fName or not os.path.exists(fName):
return
with codecs.open(fName, "r", "utf8") as f:
config.addFromFp(f, origin=origin, fName=fName)
[docs]def readConfiguration(config, systemFName, userFName):
"""fills the Configuration config with values from the the two locations.
File names that are none or point to non-existing locations are
ignored.
"""
try:
_addToConfig(config, systemFName, "system")
except ConfigError as ex:
ex.fileName = systemFName
raise
try:
_addToConfig(config, userFName, "user")
except ConfigError as ex:
ex.fileName = userFName
raise
[docs]def makeTxtDocs(config, underlineChar="."):
import textwrap
docs = []
for section in config:
if isinstance(section, MagicSection):
hdr = "Magic Section [%s]"%(section.name)
body = (section.documentation+
"\n\nThe items in this section are all of type %s. You can add keys"
" as required.\n"%
section.itemFactory.typedesc)
else:
hdr = "Section [%s]"%(section.name)
body = section.documentation
docs.append("\n%s\n%s\n\n%s\n"%(hdr, underlineChar*len(hdr),
textwrap.fill(body, width=72)))
for ci in section:
docs.append("* %s: %s; "%(ci.name, ci.typedesc))
if ci.default is not None:
docs.append(" defaults to '%s' --"%ci.default)
docs.append(textwrap.fill(ci.description, width=72, initial_indent=" ",
subsequent_indent=" "))
return "\n".join(docs)
def _getTestSuite():
"""returns a unittest suite for this module.
It's in-file since I want to keep the thing in a single file.
"""
import unittest
from io import StringIO
class TestConfigItems(unittest.TestCase):
"""tests for individual config items.
"""
def testStringConfigItemDefaultArgs(self):
ci = StringConfigItem("foo")
self.assertEqual(ci.name, "foo")
self.assertEqual(ci.value, "")
self.assertEqual(ci.description, "Undocumented")
self.assertEqual(ci.origin, "default")
ci.set("bar", "user")
self.assertEqual(ci.value, "bar")
self.assertEqual(ci.origin, "user")
self.assertEqual(ci.name, "foo")
self.assertEqual(ci.getAsString(), "bar")
def testStringConfigItemNoDefaults(self):
ci = StringConfigItem("foo", default="quux",
description="An expressionist config item")
self.assertEqual(ci.name, "foo")
self.assertEqual(ci.value, "quux")
self.assertEqual(ci.description, "An expressionist config item")
self.assertEqual(ci.origin, "default")
ci.set("None", "user")
self.assertEqual(ci.value, None)
self.assertEqual(ci.origin, "user")
self.assertEqual(ci.getAsString(), "None")
def testStringConfigItemEncoding(self):
ci = StringConfigItem("foo", default='Füße')
self.assertEqual(ci.value.encode("iso-8859-1"), b'F\xfc\xdfe')
self.assertEqual(ci.getAsString(), 'Füße')
def testIntConfigItem(self):
ci = IntConfigItem("foo")
self.assertEqual(ci.value, None)
ci = IntConfigItem("foo", default="0")
self.assertEqual(ci.value, 0)
ci.set("42")
self.assertEqual(ci.value, 42)
self.assertEqual(ci.getAsString(), "42")
def testBooleanConfigItem(self):
ci = BooleanConfigItem("foo", default="0")
self.assertEqual(ci.value, False)
self.assertEqual(ci.getAsString(), "False")
ci.set("true")
self.assertEqual(ci.value, True)
ci.set("on")
self.assertEqual(ci.value, True)
self.assertEqual(ci.getAsString(), "True")
self.assertRaises(ParseError, ci.set, "undecided")
def testEnumeratedConfigItem(self):
ci = EnumeratedConfigItem("foo", options=["bar", "foo", "Fuß"])
self.assertEqual(ci.value, "bar")
self.assertRaises(ParseError, ci.set, "quux")
ci.set('Fuß')
self.assertEqual(ci.value, "Fuß")
self.assertEqual(ci.getAsString(), 'Fuß')
def getSampleConfig():
return Configuration(
DefaultSection("General Settings",
StringConfigItem("emptyDefault", description="is empty by default"),
StringConfigItem("fooDefault", default="foo",
description="is foo by default"),),
Section("types", "Various Types",
IntConfigItem("count", default="0", description=
"is an integer"),
ListConfigItem("enum", description="is a list",
default="foo, bar"),
IntListConfigItem("intenum", description="is a list of ints",
default="1,2,3"),
DictConfigItem("map", description="is a mapping",
default="intLit:1, floatLit:0.1, bla: wurg"),))
class ReadConfigTest(unittest.TestCase):
"""tests for reading complete configurations.
"""
def testDefaults(self):
config = getSampleConfig()
self.assertEqual(config.get("emptyDefault"), "")
self.assertEqual(config.get("fooDefault"), "foo")
self.assertEqual(config.get("types", "count"), 0)
self.assertEqual(config.get("types", "enum"), ["foo", "bar"])
self.assertEqual(config.get("types", "intenum"), [1,2,3])
self.assertEqual(config.get("types", "map"), {"intLit": "1",
"floatLit": "0.1", "bla": "wurg"})
def testSetting(self):
config = getSampleConfig()
config.set("emptyDefault", "foo")
self.assertEqual(config.get("emptyDefault"), "foo")
self.assertEqual(config.getitem("emptydefault").origin, "user")
self.assertEqual(config.getitem("foodefault").origin, "default")
def testReading(self):
config = getSampleConfig()
config.addFromFp(StringIO("[general]\n"
"emptyDefault: bar\n"
"fooDefault: quux\n"
"[types]\n"
"count: 7\n"
"enum: one, two,three:3\n"
"intenum: 1, 1,3,3\n"
"map: Fuß: y, x:Fuß\n"))
self.assertEqual(config.get("emptyDefault"), "bar")
self.assertEqual(config.get("fooDefault"), "quux")
self.assertEqual(config.get("types", "count"), 7)
self.assertEqual(config.get("types", "enum"), ["one", "two", "three:3"])
self.assertEqual(config.get("types", "intenum"), [1,1,3,3])
self.assertEqual(config.get("types", "map"), {'Fuß': "y",
"x": 'Fuß'})
self.assertEqual(config.getitem("types", "map").origin, "user")
def testRaising(self):
config = getSampleConfig()
self.assertRaises(BadConfigValue, config.addFromFp,
StringIO("[types]\nintenum: brasel\n"))
self.assertRaises(SyntaxError, config.addFromFp,
StringIO("intenum: brasel\n"))
self.assertRaises(ParseError, config.getitem("types", "count").set,
"abc")
def testWarning(self):
config = getSampleConfig()
with warnings.catch_warnings(record=True) as w:
config.addFromFp(StringIO("[types]\nnonexisting: True\n"))
self.assertEqual(len(w), 1)
self.assertEqual(str(w[0].message),
"Unknown configuration item [types] nonexisting ignored.")
class MagicFactoryTest(unittest.TestCase):
"""tests for function of MagicFactories.
"""
def testMagic(self):
config = Configuration(
MagicSection("profiles", "Some magic Section",
defaults=(('a', 'b'), ('c', 'd'))))
self.assertEqual(config.get('profiles', 'c'), 'd')
self.assertRaises(NoConfigItem, config.get, 'profiles', 'd')
config.set('profiles', 'new', 'shining', origin="user")
item = config.getitem('profiles', 'new')
self.assertEqual(item.value, 'shining')
self.assertEqual(item.origin, 'user')
class UserConfigTest(unittest.TestCase):
"""tests for extraction of user-supplied config items.
"""
def testNoUserConfig(self):
config = getSampleConfig()
cp = config.getUserConfig()
self.assertEqual(cp.sections(), [])
def testSomeUserConfig(self):
config = getSampleConfig()
config.set("emptyDefault", "not empty any more")
config.set("types", "count", "4")
config.set("types", "intenum", "3,2,1")
cp = config.getUserConfig()
self.assertEqual([s for s in sorted(cp.sections())],
["general", "types"])
self.assertEqual(len(cp.items("general")), 1)
self.assertEqual(len(cp.items("types")), 2)
self.assertEqual(cp.get("general", "emptyDefault"), "not empty any more")
self.assertEqual(cp.get("types", "count"), "4")
self.assertEqual(cp.get("types", "intenum"), "3, 2, 1, ")
l = locals()
tests = [l[name] for name in l
if isinstance(l[name], type) and issubclass(l[name], unittest.TestCase)]
loader = unittest.TestLoader()
suite = unittest.TestSuite([loader.loadTestsFromTestCase(t)
for t in tests])
return suite, tests
[docs]def load_tests(loader, tests, ignore):
import doctest
import fancyconfig
suite, _ = _getTestSuite()
tests.addTest(suite)
tests.addTest(doctest.DocTestSuite(fancyconfig))
return tests
if __name__=="__main__": # pragma: no-cover
import unittest
suite = unittest.TestSuite()
unittest.TextTestRunner().run(load_tests(None, suite, None))