1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307
|
class ConfigException(Exception):
pass
class InvalidConfig(ConfigException):
pass
class OptionRequired(InvalidConfig):
def __init__(self, name, option):
ConfigException.__init__(self, 'Required option {!r} not set'.format(name))
self.name = name
self.option = option
class ConstraintException(InvalidConfig):
def __init__(self, message, constraint):
InvalidConfig.__init__(self, message)
self.constraint = constraint
class RequirementNotSatisfied(ConstraintException):
pass
class UnsupportedConfiguration(ConstraintException):
pass
class InvalidOption(ConfigException):
def __init__(self, name):
ConfigException.__init__(self, 'Invalid option {!r}'.format(name))
self.name = name
def identity(x):
return x
def one_of(choices):
def validator(x):
if x not in choices:
raise ValueError('invalid choice {!r}, expected one of {!r}', x, choices)
validator.__config__doc__ = 'One of: {!r}'.format(choices)
return validator
class ConfigOption(object):
def __init__(self, description, converter=None, default=None, required=False, help=None):
self.converter = converter
if self.converter is None:
self.converter = identity
self.description = description
self.default = default
self.required = required
self.help = help
try:
self.description = '{}. {}'.format(
self.description.rstrip('. '),
self.converter.__config_doc__
)
except AttributeError:
pass
if self.default and self.required:
raise ValueError(
'ConfigOption cannot have a default and be required at the same time.'
)
def to_parser_arguments(self):
args = dict(
type=self.converter,
help=self.description,
)
if self.required:
args['required'] = True
else:
if self.converter is bool:
args['action'] = 'store_false' if self.default else 'store_true'
args.pop('type')
else:
args['default'] = self.default
return args
class Constraint(object):
def validate(self, config):
pass
class RequirementConstraint(Constraint):
"""
Specifies a simple requirement constraint.
If a list of options are given (True), the requirement needs to be True as well.
This only checks for the variables boolean values.
"""
def __init__(self, options, require, error_formatter=None):
self.options = options
self.require = require
if len(self.options) == 0:
raise ValueError('At least one option required')
self.error_formatter = error_formatter
if self.error_formatter is None:
self.error_formatter = self._format_error
def validate(self, config):
if all(config[option] for option in self.options) and not config[self.require]:
raise RequirementNotSatisfied(self.error_formatter(self, config), self)
@staticmethod
def _format_error(constraint, config):
plural = len(constraint.options) > 1
options = ', '.join(constraint.options[:-1])
if options:
options = '{} and {}'.format(options, constraint.options[-1])
else:
options = constraint.options[0]
return 'option{s} {options} require{not_s} option {require}'.format(
options=options,
require=constraint.require,
s='s' if plural else '',
not_s='' if plural else 's'
)
class UnsupportedConstraint(Constraint):
"""
Specifies a unsupported constraint, that can either be a single option
or a combination of options. Checks only for the boolean value of the options.
"""
def __init__(self, given, not_allowed, error_formatter=None):
self.given = given
self.not_allowed = not_allowed
if len(self.given) == 0:
raise ValueError('At least one \'given\' option required')
self.error_formatter = error_formatter
if self.error_formatter is None:
self.error_formatter = self._format_error
def validate(self, config):
if all(config[option] for option in self.given) and config[self.not_allowed]:
raise UnsupportedConfiguration(self.error_formatter(self, config), self)
@staticmethod
def _format_error(constraint, config):
plural = len(constraint.given) > 1
given = ', '.join(constraint.given[:-1])
if given:
given = '{} and {}'.format(given, constraint.given[-1])
else:
given = constraint.given[-1]
return 'option{s} {given} can not be used together with {not_allowed}'.format(
given=given,
not_allowed=constraint.not_allowed,
s='s' if plural else ''
)
class Config(object):
"""
Base for all glad configurations. The class with initiliaze the options
with it iself. Every uppercase name will be assumed to be a configuration option
and should be of type ConfigOption:
class MyAwesomeConfig(Config):
DEBUG = ConfigOption(
converter=bool,
default=False
description='Enables debug output'
)
ITERATIONS = ConfigOption(
converter=int,
required=True
description='Number of iterations'
)
config = MyAwesomeConfig()
config['DEBUG'] = True
# update config from file
# ...
# now make sure every required option has been set
config.validate()
if config['DEBUG']:
print 'debug information'
Special Constraints can be specified in the __constraints__ variable.
class MyConstraintConfig(MyAwesomeConfig):
__constraints__ = [
RequirementConstraint(['DEBUG'], 'iterations')
]
The constraints will be checked when calling `.valid` or `.validate()`
"""
def __init__(self):
self._options = dict()
self._values = dict()
# initialize options, every uppercase name without leading underscore = option
for name in dir(self):
if name.isupper() and not name.startswith('_'):
option = self._options[name] = getattr(self, name)
if not option.required:
self._values[name] = option.default
def set(self, name, value, convert=True):
try:
option = self._options[name]
except KeyError:
raise InvalidOption(name)
if convert:
value = option.converter(value)
self._values[name] = value
def get(self, item, default=None):
try:
return self[item]
except KeyError:
return default
def __getitem__(self, item):
return self._values[item]
def __setitem__(self, key, value):
self.set(key, value, convert=True)
def items(self):
return list(self._options.items())
@property
def valid(self):
"""
Checks if every required option has been set.
:return: True if everything has been set otherwise False
"""
try:
self.validate()
except InvalidConfig:
return False
return True
def validate(self):
"""
Checks if every required option has been set.
Throws InvalidConfig if a required option is missing.
This also checks all specified constraints.
Should be overwritten by subclasses if necessary.
"""
for name, option in self._options.items():
if option.required:
if not name in self._values:
raise OptionRequired(name, option)
constraints = getattr(self, '__constraints__', [])
for constraint in constraints:
constraint.validate(self)
def update_from_object(self, obj, convert=True, ignore_additional=False):
for name in dir(obj):
if not name.startswith('_'):
try:
self.set(name, getattr(obj, name), convert=convert)
except InvalidOption:
if not ignore_additional:
raise
def init_parser(self, parser):
for name, option in self._options.items():
parser_name = '--' + name.lower().replace('_', '-')
parser.add_argument(
parser_name,
dest=name,
**option.to_parser_arguments()
)
def to_dict(self, transform=None):
if transform is None:
transform = identity
result = dict()
for name, value in self._values.items():
result[transform(name)] = value
return result
|