Last active
August 29, 2015 14:10
-
-
Save cdunklau/c9d674066cf816721c21 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import re | |
| from ConfigParser import NoOptionError | |
| class ValidationFailed(Exception): | |
| def __init__(self, reason): | |
| self.reason = reason | |
| def __str__(self): | |
| return repr(self) | |
| def __repr__(self): | |
| return '<ValidationFailed: {0!r}>'.format(self.reason) | |
| class ConfigurationTypeMaker(object): | |
| """ | |
| Simplifies the creation of configuration classes. | |
| Example usage:: | |
| from ConfigParser import RawConfigParser | |
| class _BaseFlarbleConfig(object): | |
| def __init__(self, config_filename): | |
| self.parser = RawConfigParser() | |
| self.config_filename = config_filename | |
| def load(self, file_like=None): | |
| if file_like is None: | |
| self.parser.read(self.config_filename) | |
| else: | |
| self.parser.readfp(file_like) | |
| config_type_maker = ConfigurationTypeMaker(_BaseFlarbleConfig) | |
| @config_type_maker.field('flarble', 'iterations', 1) | |
| def iterations_validator(value): | |
| try: | |
| intval = int(value) | |
| except ValueError: | |
| raise ValidationFailed( | |
| '{0!r} is not a valid integer'.format(intval) | |
| if intval < 1: | |
| raise ValidationFailed( | |
| 'Expected positive integer, got {0!r}'.format( | |
| intval) | |
| return intval | |
| FlarbleConfig = config_type_maker.make_type('FlarbleConfig') | |
| assert FlarbleConfig.field_names == ('iterations,) | |
| import StringIO | |
| config = FlarbleConfig('flarble.conf') | |
| config.load(StringIO.StringIO('[flarble]\niterations: 5\n')) | |
| # The field_names attribute is useful for validating all fields | |
| # at once, instead of just on first access. | |
| for name in config.field_names: | |
| try: | |
| getattr(config, name) | |
| except ValidationFailed as e: | |
| print 'Failed to validate field {0!r}: {1!r}'.format( | |
| name, e.reason) | |
| raise | |
| assert config.iterations == 5 | |
| # default works | |
| config = FlarbleConfig('flarble.conf') | |
| config.load(StringIO.StringIO('[flarble]\n')) | |
| assert config.iterations == 1 | |
| The type given in the constructor is used as the base type for | |
| the class to be created by the `make_type` method. Its instances | |
| must provide a ``ConfigParser`` instance as an attribute named | |
| "parser". | |
| The generated class will also be given a class attribute called | |
| `field_names`, which will be a tuple of the names of the | |
| generated properties. | |
| Option names will be used as the property names on the generated | |
| class, therefore it is an error to use names that are not Python | |
| identifiers, and the names must not conflict with attributes on | |
| the configuration class, including 'parser' and 'field_names'. | |
| """ | |
| base_type = None | |
| "Base configuration class" | |
| fields = None | |
| "Maps option_name to tuple of (section_name, default, validator)" | |
| def __init__(self, base_type): | |
| self.base_type = base_type | |
| self.fields = {} | |
| def field(self, section_name, option_name, default_value=None): | |
| """ | |
| Decorator for config field validators. | |
| :param section_name: Name of the config section | |
| :param option_name: Name of the config option | |
| :param default_value: Default value used if the field is not | |
| found; if set to None, the field is | |
| required | |
| The validator function will be called with the value to be | |
| validated and must return the converted value. If the value is | |
| not valid, the validator must raise `ValidationFailed` with a | |
| reason. | |
| """ | |
| def decorator(validator): | |
| if not re.match(r'[a-zA-Z_][a-zA-Z0-9-]*$', option_name): | |
| raise Exception( | |
| 'Option name must be a valid Python identifier, ' | |
| 'got {0!r}'.format(option_name)) | |
| if option_name in self.fields: | |
| raise Exception( | |
| 'Duplicate option name {0}'.format(option_name)) | |
| if hasattr(self.base_type, option_name): | |
| raise Exception( | |
| 'Option name {0!r} reserved by base config class'.format( | |
| option_name)) | |
| if option_name in ('parser', 'field_names'): | |
| raise Exception('Reserved option name {0}'.format(option_name)) | |
| self.fields[option_name] = ( | |
| section_name, default_value, validator) | |
| return validator | |
| return decorator | |
| def make_type(self, new_type_name): | |
| type_dict = {} | |
| field_names = [] | |
| for confopt, (confsec, default, validator) in self.fields.items(): | |
| prop = _make_config_property(confsec, confopt, default, validator) | |
| name = prop.fget.__name__ | |
| type_dict[name] = prop | |
| field_names.append(name) | |
| type_dict['field_names'] = tuple(field_names) | |
| return type(new_type_name, (self.base_type,), type_dict) | |
| def _make_config_property(section_name, option_name, default_value, validator): | |
| def getter(self): | |
| try: | |
| tovalidate = self.parser.get(section_name, option_name) | |
| except NoOptionError: | |
| if default_value is not None: | |
| return default_value | |
| else: | |
| raise ValidationFailed( | |
| 'Missing required option {0!r} in section {1!r}'.format( | |
| option_name, section_name)) | |
| return validator(tovalidate) | |
| getter.__doc__ = validator.__doc__ | |
| getter.__name__ = option_name | |
| def setter(self, value): | |
| # TODO This doesn't work, we can't use the same naive validator | |
| # in both directions. Perhaps the validator should accept two args, | |
| # keyword args, or do we need two functions? | |
| validated = validator(value) | |
| if not self.parser.has_section(section_name): | |
| self.parser.add_section(section_name) | |
| self.parser.set(section_name, option_name, validated) | |
| setter.__doc__ = validator.__doc__ | |
| setter.__name__ = option_name + '_setter' | |
| return property(getter, setter) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import unittest | |
| from StringIO import StringIO | |
| from ConfigParser import RawConfigParser | |
| from config_type_maker import ( | |
| ConfigurationTypeMaker, ValidationFailed, _make_config_property) | |
| class ConfigurationTypeMakerTestCase(unittest.TestCase): | |
| def test_reminder(self): | |
| self.fail('Add tests here') | |
| def test_constructor(self): | |
| self.fail('not done') | |
| ctm = ConfigurationTypeMaker() | |
| def foovalidator_toupper(value): | |
| """ | |
| Validate that the value equals 'foo', and return it uppercased. | |
| """ | |
| if value != 'foo': | |
| raise ValidationFailed("{0!r} is not equal to 'foo'".format(value)) | |
| return value.upper() | |
| class MakeConfigPropertyTestCase(unittest.TestCase): | |
| def setUp(self): | |
| class ConfigStub(object): | |
| def __init__(self, conftext): | |
| self.parser = RawConfigParser() | |
| self.parser.readfp(StringIO(conftext)) | |
| self.config_class = ConfigStub | |
| def config_with_must_be_foo_property(self, default_value, config_text): | |
| self.config_class.must_be_foo = _make_config_property( | |
| 'main', 'must_be_foo', default_value, foovalidator_toupper) | |
| return self.config_class(config_text) | |
| def test_getter_gets_validated_value(self): | |
| cfg = self.config_with_must_be_foo_property( | |
| None, '[main]\nmust_be_foo: foo\n') | |
| self.assertEqual('FOO', cfg.must_be_foo) | |
| def test_getter_raises_on_bad_value(self): | |
| cfg = self.config_with_must_be_foo_property( | |
| None, '[main]\nmust_be_foo: notfoo\n') | |
| pattern = r"is not equal to 'foo'" | |
| with self.assertRaisesRegexp(ValidationFailed, pattern): | |
| cfg.must_be_foo | |
| def test_getter_raises_on_missing_option(self): | |
| cfg = self.config_with_must_be_foo_property( | |
| None, '[main]\nignored: blah\n') | |
| pattern = r"Missing required option 'must_be_foo' in section 'main'" | |
| with self.assertRaisesRegexp(ValidationFailed, pattern): | |
| cfg.must_be_foo | |
| def test_getter_returns_default_on_missing_option(self): | |
| cfg = self.config_with_must_be_foo_property( | |
| 'default value', '[main]\nignored: blah\n') | |
| self.assertEqual('default value', cfg.must_be_foo) | |
| def test_setter_sets_validated_value(self): | |
| cfg = self.config_with_must_be_foo_property( | |
| None, '[main]\nignored: blah\n') | |
| cfg.must_be_foo = 'foo' | |
| self.assertEqual('FOO', cfg.must_be_foo) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment