There comes a time in every programmer’s life when they decide that some silly, common library that they use all the time just isn’t good enough. It takes too many actions, or it feels opaque, or there are obvious features conspicuous in their absence.
For me, that time is now, that library is ConfigParser, and the replacement is included below the fold. It’s called conf, and it means that the only interaction you as a programmer are required to have with your config file is to assign and/or read values to/from it. Assigning and reading look exactly like normal attribute assignment/reading.
Subversion/TRAC string: https://svn.coriolinus.net/OOconf
distutils packaged version: conf-0.2.0
As always, I’m too cheap to pay a thousand dollars for a site certificate, so please ignore any certificate mismatch errors you encounter when viewing the https side of the site. If you don’t trust me enough to click through, you’ll still find the current version under the fold.
import os
import codecs
import json
from ConfigParser import SafeConfigParser
def internal(func):
def tf(self, *args, **kwargs):
self.__dict__['__conf__'].__dict__['__internal__'] += 1
try:
return func(self, *args, **kwargs)
finally:
self.__dict__['__conf__'].__dict__['__internal__'] -= 1
return tf
class Section(object):
def __init__(self, conf, name, fullname=None):
if fullname is None:
fullname = name
self.__dict__['__conf__'] = conf
self.__name__ = name
self.__fullname__ = fullname
self.__dict__['subsections'] = set()
self.__dict__['attributes'] = set()
#this lines MUST be the last in __init__
self.__restrictedvars__ = set((i for i in dir(self) if '__' not in i))
@internal
def __setattr__(self, name, value):
if self.restricted(name):
if (not self.__conf__.__restrict__) or self.__conf__.__internal__ > 0:
self.__dict__[name] = value
else:
raise AttributeError("Namespace conflict: %s restricted for Conf use." % name)
else:
self.__conf__.__new_data__ = True
self.attributes.add(name)
self.__conf__.__cp__.set(self.__fullname__, name, json.dumps(value))
@internal
def __getattr__(self, name):
if self.restricted(name):
return self.__dict__[name]
else:
return json.loads(self.__conf__.__cp__.get(self.__fullname__, name))
@internal
def __delattr__(self, name):
if self.restricted(name):
if (not self.__conf__.__restrict__) or self.__conf__.__internal__ > 0:
del self.__dict__[name]
else:
raise AttributeError("Namespace conflict: %s restricted for Conf use." % name)
else:
self.__conf__.__new_data__ = True
self.attributes.remove(name)
self.__conf__.__cp__.remove_option(self.__name__, name)
@internal
def restricted(self, name):
"""
This function returns true for all attributes which should be stored locally, not in the
configuration file proper.
"""
if name.startswith('__') or name.endswith('__'):
return True
if name in self.__restrictedvars__:
return True
return False
@internal
def add_section(self, name):
fullname = ''.join((self.__fullname__, '.', name))
if hasattr(self, name):
raise ValueError("Namespace conflict: %s already in use" % fullname)
if not self.__conf__.__cp__.has_section(fullname):
self.__conf__.__cp__.add_section(fullname)
self.subsections.add(name)
self.__dict__[name] = Section(self.__conf__, name, fullname)
self.__restrictedvars__.add(name)
self.__new_data__ = True
@internal
def remove_section(self, name):
fullname = ''.join((self.__fullname__, '.', name))
if not hasattr(self, name):
raise ValueError("Can't remove section %s, as it doesn't exist" % fullname)
sub = getattr(self, name)
for subsub in list(sub.subsections):
sub.remove_section(subsub)
self.__conf__.__cp__.remove_section(fullname)
self.subsections.remove(name)
self.__restrictedvars__.remove(name)
del self.__dict__[name]
self.__new_data__ = True
class Conf(Section):
"""
Automatic storage and retrieval of arbitrary values into a config file.
Uses type information and automatic reconversions to store a variety of primitive types in a
perfectly human-readable format. Primitive types are those encodable by the json module.
Usage:
>>> from conf import Conf
>>> conf = Conf() #or Conf(filename, defaultsectionname)
>>> conf.foo = 'hello world'
>>> conf.bar = 723
>>> conf.baz = False
>>> conf.flush()
[exit, start a new session here]
>>> conf = Conf()
>>> conf.foo
'hello world'
>>> conf.bar
723
>>> conf.baz
False
If you want implicit file creation, you need to use a with statement:
>>> with Conf() as conf:
... conf.foo = 2783.1
...
>>> del conf
>>> with Conf() as otherConf:
... otherConf.foo
...
2783.1
"""
def __init__(self, filename ='.conf', sectionName=None):
"""
Initialize a new Conf object.
"""
self.__dict__['__internal__'] = 0
self.__dict__['__restrict__'] = False
#the above is magic; it must come first
#sanity check
if sectionName is not None and '.' in sectionName:
raise ValueError("Namespace: '.' cannot be part of a section name")
#initialize the superclass
Section.__init__(self, self, sectionName if sectionName is not None else 'config')
self.__filename__ = filename
self.__cp__ = SafeConfigParser()
#load and initialize the configparser
if os.path.exists(filename):
with codecs.open(filename, 'rb', 'utf8') as cf:
self.__cp__.readfp(cf, filename)
if sectionName is None:
topLevelSections = [s for s in self.__cp__.sections() if s.count('.') == 0]
if len(topLevelSections) == 1:
sectionName = topLevelSections[0]
self.__name__ = sectionName
self.__fullname__ = sectionName
else:
sectionName = 'config'
#create the default section
if not self.__cp__.has_section(sectionName):
self.__cp__.add_section(sectionName)
#load default section attributes
self.attributes = set(self.__cp__.options(sectionName))
#load the various sections
#first, sort them by the number of dots they contain
secs = [(s.count('.'), s) for s in self.__cp__.sections() if s != sectionName]
secs.sort()
for name in [sec for count, sec in secs]:
if '.' not in name:
self.subsections.add(name)
self.__dict__[name] = Section(self, name)
self.__dict__[name].attributes = set(self.__cp__.options(name))
self.__restrictedvars__.add(name)
else:
fullname = name
rest, name = name.rsplit('.', 1)
sec = self
for part in rest.split('.'):
sec = getattr(sec, part)
sec.subsections.add(name)
sec.__dict__[name] = Section(self, name, fullname)
sec.__dict__[name].attributes = set(self.__cp__.options(fullname))
sec.__restrictedvars__.add(name)
self.__new_data__ = False
#this must come last:
self.__internal__ = 0
self.__restrict__ = True
#This is used in combination with the @internal decorator. Each method so decorated
# increments this variable on entry, and decrements it on exit. They can then check: is
# __internal__ > 0? If yes, they were called from within this Conf object, and can adjust
# their behavior accordingly.
#
#Note that we initialize it here to 0. When this __init__ exits, it decrements to -1. This
# is intentional. Since each internal function starts by incrementing it, this means that
# only if the variable is > 0 was its caller also internal.
@internal
def __enter__(self):
return self
@internal
def __exit__(self, exc_type, exc_value, traceback):
if self.__new_data__:
self.flush()
@internal
def flush(self):
with codecs.open(self.__filename__, 'wb', 'utf8') as cf:
self.__cp__.write(cf)
self.__new_data__ = False
@internal
def add_section(self, name):
"""
Create a new section in the conf file. This will become a dotted extension of the conf object.
For example:
>>> c = Conf()
>>> c.foo = 'hello world'
>>> c.add_section('bar')
>>> c.bar.baz = 'world says hello'
The above turns into a config file which looks like this:
[config]
foo = hello world
[bar]
baz = world says hello
"""
if hasattr(self, name):
raise ValueError("Namespace conflict: %s already in use" % name)
if '.' in name:
raise ValueError("Namespace: '.' cannot be part of a section name")
if not self.__conf__.__cp__.has_section(name):
self.__conf__.__cp__.add_section(name)
self.subsections.add(name)
self.__dict__[name] = Section(self, name)
self.__restrictedvars__.add(name)
self.__new_data__ = True
@internal
def remove_section(self, name):
"""
Remove a section and all included data.
"""
if not hasattr(self, name):
raise ValueError("Can't remove section %s, as it doesn't exist" % name)
sub = getattr(self, name)
for subsub in sub.subsections:
sub.remove_section(subsub)
self.__cp__.remove_section(name)
self.subsections.remove(name)
self.__restrictedvars__.remove(name)
del self.__dict__[name]
self.__new_data__ = True

conf.py by coriolinus is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
Permissions beyond the scope of this license may be available at http://www.coriolinus.net/contact/.
No comments yet.