I have written a simple yaml based config parser in Python. The idea is that it will get passed around in other scripts after being initialized with a config file within a main function.
I am interested if this code is OK with regards to the following
- general best coding practices
- interface design
- test design
- should check for more corner cases, e.g. in the section below
# allowed values
, should I check first ifself.config[key]
exists? Or are the python exceptions that will be raised in such a case sufficient?
The config parser:
# config_parser.py
import yaml
"""
An attempt at a simple yaml based config parser class
"""
class IllegalConfigValue(Exception): pass
class MissingConfigValue(Exception): pass
class configParser(object):
"""
Read a yaml file and provide the options defined therein as attributes.
Allow setting of defaults, allowed values and required keys.
"""
def __init__(self, config_file, defaults={}, allowed_values={}, required_keys=[]):
"""
Enhance the simpleConfigParser by adding defaults and allowed_values
config_file: path to a yaml file
defaults: a dict containing default values. optional
allowed_values: a dict containing lists of allowed values. optional
required_keys: a list of keys that must be contained in the yaml file.
"""
# defaults
for key,value in defaults.items():
setattr(self,key,value)
self.config_file = config_file
# parsing config file
with open(self.config_file, 'r') as cfile:
self.config = yaml.safe_load(cfile)
for key,value in self.config.items():
setattr(self,key,value)
# allowed values
for key,value in allowed_values.items():
if self.config[key] not in value:
raise IllegalConfigValue('Illegal configuration value for "' +
str(key) + '": ' + str(self.config[key]))
# required keys
for key in required_keys:
if not self.__dict__.get(key):
raise MissingConfigValue('The following key is missing in ' +
self.config_file + ': ' + str(key))
The test code:
# test_config_parser.py
# tests for config_parser.py
import os
import unittest
import yaml
from .. import config_parser
class testConfigParser(unittest.TestCase):
@classmethod
def setUp(self):
"""Create a config file used for parsing"""
self.config_file = '/tmp/test_config.yml'
config = {
'animal': 'cat',
'location': '/tmp/foobar.txt',
'values': ['a', 'b', 'c', 23],
}
with open(self.config_file, 'w') as filehandle:
filehandle.write(yaml.dump(config, default_flow_style=False,))
@classmethod
def tearDown(self):
"""Delete test config file"""
os.unlink(self.config_file)
def test_configParser_basic(self):
"""Basic interface test"""
config = config_parser.configParser(self.config_file)
self.assertEqual(config.config['animal'], 'cat')
self.assertEqual(config.animal, 'cat')
self.assertListEqual(config.values, ['a', 'b', 'c', 23])
def test_configParser_default(self):
"""Setting defaults"""
config = config_parser.configParser(
self.config_file,
defaults = {'foobar': 'barfoo', 'animal': 'dog'}
)
# defaults do not overwrite the provided values
self.assertEqual(config.config['animal'], 'cat')
self.assertEqual(config.animal, 'cat')
self.assertListEqual(config.values, ['a', 'b', 'c', 23])
# if no value is provided, the default kicks in
self.assertEqual(config.foobar, 'barfoo')
def test_configParser_allowed_values(self):
"""Allowed values"""
config = config_parser.configParser(
self.config_file,
allowed_values = {'animal': ['cat', 'dog', 'snake']}
)
with self.assertRaises(config_parser.IllegalConfigValue):
config_parser.configParser(
self.config_file,
allowed_values = {'animal': ['dog', 'snake']})
def test_configParser_required_keys(self):
"""Required keys"""
config = config_parser.configParser(
self.config_file,
required_keys = ['animal', 'location']
)
with self.assertRaises(config_parser.MissingConfigValue):
config_parser.configParser(
self.config_file,
required_keys = ['animal', 'location', 'foobar'])