Skip to content

conf.py: One-upping ConfigParser

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

Creative Commons License
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/.

RSS feed

Comments

No comments yet.

Sorry, the comment form is closed at this time.