2
\$\begingroup\$

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 if self.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'])
\$\endgroup\$
1

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.