#!/usr/bin/env python
"""
A simple, pythonic configuration parser.
Alberto Bertogli (albertito@blitiri.com.ar)
It parses files of the form:
------------------ 8< ------------------ 8< ------------------
key1 = value1
key2 = 3453
group1.key1 = 26.56
group1.key2 = 01.59
------------------ 8< ------------------ 8< ------------------
It knows the available keys, and can check for data types and parse
accordingly. It supports default values.
Use it like:
# Define the available keys, their types, and the default values
settings = (
	('key1', str),
	('key2', int),
	('key3', str, 'default value'),
	('group1.key1', float),
	('group1.key2', float, 9283),
)
# Parse the given filename
config = Config('config', settings)
# Use the results in a python-friendly way
print config.key1
print config.group1.key2
"""
# Use new style objects everywhere
__metaclass__ = type
class ConfigError (Exception):
	pass
class Config:
	def __init__(self, filename = None, settings = ()):
		if not filename:
			return
		# settings is a list of either:
		# - name or (name,), which means mandatory string
		# - (name, class), which means mandatory of the given class
		# - (name, class, default_value), which means optional of the
		#	 given class, with the given default
		mandatory = {}
		optional = {}
		have_set = []
		for i in settings:
			if isinstance(i, str):
				mandatory[i] = str
			elif len(i) == 1:
				mandatory[i[0]] = str
			elif len(i) == 2:
				mandatory[i[0]] = i[1]
			elif len(i) == 3:
				optional[i[0]] = (i[1], i[2])
		try:
			f = open(filename)
		except:
			desc = "Can't find config file: %s" % filename
			raise ConfigError, desc
		count = 0
		for l in f:
			count += 1
			l = l.strip()
			if (not l) or l.startswith('#'):
				continue
			if '=' in l:
				name, val = l.split('=', 1)
				name = name.strip()
				name = name.replace('\s', '_')
				val = val.strip()
				if name in mandatory:
					val = self._convert_to(name, val,
							mandatory[name], count)
				elif name in optional:
					val = self._convert_to(name, val,
							optional[name][0],
							count)
				have_set.append(name)
				self._setattr(name, val, count)
		for i in mandatory.keys():
			if i not in have_set:
				desc = "Mandatory option %s is not set" % i
				raise ConfigError, desc
		for (name, (cls, default)) in optional.iteritems():
			if name not in have_set:
				self._setattr(name, default, -1)
	def _convert_to(self, name, val, cls, lineno):
		try:
			return cls(val)
		except:
			desc = "Line %d: " % lineno
			desc += "Error converting %s " % name
			desc += "to mandatory data type %s" % cls
			raise ConfigError, desc
	def _setattr(self, name, val, lineno):
		if '.' in name:
			parent, name = name.split('.', 1)
			try:
				obj = getattr(self, parent)
			except AttributeError:
				obj = Config()
				setattr(self, parent, obj)
			obj._setattr(name, val, lineno)
		else:
			obj = getattr(self, name, None)
			if isinstance(obj, Config):
				desc = "Line %d: " % lineno
				desc += "Overriding a group with a value"
				raise ConfigError, desc
			setattr(self, name, val)