0001"""
0002**Probably will be deprecated** (13 Aug 2006): I'm not sure if this
0003is worth keeping around.
0004
0005
0006.ini file schemas
0007
0008You can define a schema for your .ini configuration files, which
0009defines the types and defaults for the values. You can also define
0010a catchall attribute.
0011
0012TODO
0013----
0014
0015Currently, this does not deal with sections at all, and sections are
0016not allowed. That wil take some more thought, as the schemas will
0017probably be per-section (though one section may inherit from another,
0018or defaults may be inherited, etc, which should be allowed for).
0019
0020Documentation isn't kept track of, nor is it generated. It should
0021possible to indicate with ``opt(help=...)``, and as an option
0022to ``INISchema.ini_repr()`` you should be able to put in documentation
0023in comments.
0024
0025Comments aren't kept track of.
0026
0027Key order isn't kept track of.
0028
0029Minimal .ini files should be possible to generate -- only generating
0030keys when the default doesn't match the actual value.
0031
0032There should be a way to check that the entire config is loaded, and
0033there are no missing values (options which weren't set and have no
0034default).
0035
0036Usage
0037-----
0038
0039::
0040
0041 class VHostSchema(INISchema):
0042
0043 server_name = opt()
0044 port = optint(default=80)
0045 # optlist means this can show up multiple times:
0046 server_alias = optlist(default=[])
0047 document_root = opt()
0048
0049 vhost = VHostSchema()
0050 vhost.load('config.ini')
0051 connect(vhost.server_name, vhost.port)
0052 # etc.
0053
0054Any schema can contain an ``optdefault()`` object, which will
0055pick up any keys that aren't specified otherwise (if not present,
0056extra keys are an error). Then you'll get a dictionary of lists,
0057for all the extra config values. (Use
0058``optdefault(allow_multiple=False)`` if you want a dictionary of
0059strings).
0060
0061If you expect multiple values, use ``optlist(subtype=optsomething())``
0062for a key. The values will be collected in a list; if you don't
0063indicate ``subtype`` then ``opt()`` is used. Other types are
0064fairly easy to make through subclassing.
0065
0066You can generate config files by using ``schema.ini_repr()`` which
0067will return a string version of the ini file.
0068
0069Implementation
0070--------------
0071
0072This makes heavy use of descriptors. If you are not familiar with
0073descriptors, see http://users.rcn.com/python/download/Descriptor.htm
0074
0075It also makes light use of metaclasses.
0076"""
0077
0078
0079import iniparser
0080
0081class INIMeta(type):
0082
0083 def __new__(meta, class_name, bases, d):
0084 cls = type.__new__(meta, class_name, bases, d)
0085 cls.__classinit__.im_func(cls, d)
0086 return cls
0087
0088class ParseValueError(Exception):
0089 pass
0090
0091class NoDefault:
0092 pass
0093
0094class INISchema(object):
0095
0096 __metaclass__ = INIMeta
0097
0098 _default_option = None
0099 _default_values = None
0100
0101 _config_names = {}
0102 _config_names_lower = {}
0103
0104 case_insensitive = False
0105
0106 def __classinit__(cls, d):
0107
0108 if cls.__bases__ == (object,):
0109 return
0110 cls._config_names = cls._config_names.copy()
0111 cls._config_names_lower = cls._config_names_lower.copy()
0112 for name, value in d.items():
0113 if isinstance(value, opt):
0114 cls.add_option(name, value)
0115
0116
0117
0118 def add_option(cls, attr_name, option):
0119 """
0120 Classmethod: add the option using the given attribute name.
0121 This can be called after the class has been created, to
0122 dynamically build up the options.
0123 """
0124 if isinstance(option, optdefault):
0125
0126
0127 cls._default_option = [option]
0128 option.attr_name = attr_name
0129 return
0130 if option.names is None:
0131 option.names = [attr_name]
0132 option.attr_name = attr_name
0133 for option_name in option.names:
0134 cls._config_names[option_name] = option
0135 cls._config_names_lower[option_name.lower()] = option
0136 option.set_schema(cls)
0137 setattr(cls, attr_name, option)
0138
0139 add_option = classmethod(add_option)
0140
0141 def __init__(self):
0142 self._ini_attrs = {}
0143
0144 def set_config_value(self, name, value):
0145 if self.case_insensitive:
0146 name = name.lower()
0147 config_names = self._config_names_lower
0148 else:
0149 config_names = self._config_names
0150 if config_names.has_key(name):
0151 setattr(self, config_names[name].attr_name, value)
0152 elif not self._default_option:
0153 raise ParseValueError(
0154 "The setting %r was not expected (from %s)"
0155 % (name, ', '.join(config_names.keys()) or 'none'))
0156 else:
0157 self._default_option[0].set_config_value(
0158 self, name, value)
0159
0160 def _parser(self):
0161 return SchemaINIParser(self)
0162
0163 def load(self, filename, **kw):
0164 """
0165 Loads the filename. Use the encoding keyword argument to
0166 specify the file's encoding.
0167 """
0168 self._parser().load(filename, **kw)
0169
0170 def loadstring(self, string, **kw):
0171 """
0172 Loads the string, which is the content of the ini files.
0173 Use the filename keyword argument to indicate the filename
0174 source (or another way to identify the source of the string
0175 in error messages).
0176 """
0177 self._parser().loadstring(string, **kw)
0178
0179 def as_dict(self, fold_defaults=False):
0180 """
0181 Returns the loaded configuration as a dictionary.
0182 """
0183
0184 v = self._ini_attrs.copy()
0185 if fold_defaults:
0186 if self._default_values:
0187 v.update(self._default_values)
0188 elif self._default_option:
0189 v[self._default_option[0].attr_name] = self._default_values or {}
0190 return v
0191
0192 def ini_repr(self):
0193 """
0194 Returns the loaded values as a string, suitable as a
0195 configuration file.
0196 """
0197 config_names = []
0198 used_options = {}
0199 for option in self._config_names.values():
0200 if used_options.has_key(option):
0201 continue
0202 used_options[option] = None
0203 config_names.append((option.names[0], option))
0204 config_names.sort()
0205 if self._default_option:
0206 config_names.append((None, self._default_option[0]))
0207 result = []
0208 for name, option in config_names:
0209 result.append(option.ini_assignment(
0210 self, getattr(self, option.attr_name)))
0211 return ''.join(result)
0212
0213class opt(object):
0214
0215 default = NoDefault
0216
0217 def __init__(self, names=None, **kw):
0218 self.names = names
0219 self.attr_name = None
0220 self.schema = None
0221 for name, value in kw.items():
0222 if not hasattr(self, name):
0223 raise TypeError(
0224 "The keyword argument %s is unknown"
0225 % name)
0226 setattr(self, name, value)
0227
0228 def set_schema(self, schema):
0229 self.schema = schema
0230
0231 def __get__(self, obj, type=None):
0232 if obj is None:
0233 return self
0234 try:
0235 return obj._ini_attrs[self.attr_name]
0236 except KeyError:
0237 if self.default is not NoDefault:
0238 return self.default
0239 raise AttributeError(
0240 "The attribute %s has not been set on %r"
0241 % (self.attr_name, obj))
0242
0243 def __set__(self, obj, value):
0244 new_value = self.convert(obj, value)
0245 self.validate(obj, new_value)
0246 self.set_value(obj, new_value)
0247
0248 def convert(self, obj, value):
0249 return value
0250
0251 def validate(self, obj, value):
0252 pass
0253
0254 def set_value(self, obj, value):
0255 obj._ini_attrs[self.attr_name] = value
0256
0257 def __delete__(self, obj):
0258 try:
0259 del obj._ini_attrs[self.attr_name]
0260 except KeyError:
0261 raise AttributeError(
0262 "%r does not have an attribute %s"
0263 % (obj, self.attr_name))
0264
0265 def ini_assignment(self, obj, value, name=None):
0266 if name is None:
0267 name = self.names[0]
0268 return '%s=%s\n' % (name,
0269 self.ini_fold(self.ini_repr(obj, value)))
0270
0271 def ini_fold(self, value):
0272 lines = value.splitlines()
0273 if len(lines) <= 1:
0274 return value
0275 lines = [lines[0]] + [' ' + l for l in lines[1:]]
0276 return '\n'.join(lines)
0277
0278 def ini_repr(self, obj, value):
0279 return str(value)
0280
0281optstring = opt
0282
0283class optint(opt):
0284
0285 max = None
0286 min = None
0287
0288 bad_type_message = "You must give an integer number, not %(value)r"
0289 too_large_message = "The value %(value)s is too large"
0290 too_small_message = "The value %(value)s is too small"
0291 coerce = int
0292
0293 def convert(self, obj, value):
0294 try:
0295 new_value = self.coerce(value)
0296 except ValueError:
0297 raise ParseValueError(
0298 self.bad_type_message % {'value': value})
0299 if self.max is not None and new_value > self.max:
0300 raise ParseValueError(
0301 self.too_large_message % {'value': value})
0302 if self.min is not None and new_value < self.min:
0303 raise ParseValueError(
0304 self.too_small_message % {'value': value})
0305 return new_value
0306
0307class optfloat(optint):
0308
0309 coerce = float
0310 bad_type_message = "You must give a float number, not %(value)r"
0311
0312class optbool(opt):
0313
0314 true_values = ('yes', 'true', '1', 'on')
0315 false_values = ('no', 'false', '0', 'off')
0316
0317 def convert(self, obj, value):
0318 if value.lower() in self.true_values:
0319 return True
0320 elif value.lower() in self.false_values:
0321 return False
0322 else:
0323 raise ParseValueError(
0324 "Should be a boolean value (true/false, on/off, yes/no), not %r"
0325 % value)
0326
0327 def ini_repr(self, obj, value):
0328 if value:
0329 return 'true'
0330 else:
0331 return 'false'
0332
0333class optlist(opt):
0334
0335 subtype = opt
0336
0337 def __init__(self, *args, **kw):
0338 opt.__init__(self, *args, **kw)
0339 if isinstance(self.subtype, type):
0340 self.subtype = self.subtype()
0341
0342 def convert(self, obj, value):
0343 return self.subtype.convert(obj, value)
0344
0345 def validate(self, obj, value):
0346 self.subtype.validate(obj, value)
0347
0348 def set_value(self, obj, value):
0349 obj._ini_attrs.setdefault(self.attr_name, []).append(value)
0350
0351 def ini_assignment(self, obj, value, name=None):
0352 if name is None:
0353 name = self.names[0]
0354 assert not isinstance(value, (str, unicode)), (
0355 "optlist attributes should receive lists or sequences, not "
0356 "strings (%r)" % value)
0357 all = [self.subtype.ini_assignment(obj, sub, name=name)
0358 for sub in value]
0359 return ''.join(all)
0360
0361class optdefault(opt):
0362
0363 allow_multiple = True
0364
0365 def set_config_value(self, obj, name, value):
0366 if obj._default_values is None:
0367 obj._default_values = {}
0368 if self.allow_multiple:
0369 obj._default_values.setdefault(name, []).append(value)
0370 else:
0371 if obj._default_values.has_key(name):
0372 raise ParseValueError(
0373 "You have already set the configuration key %r"
0374 % name)
0375 obj._default_values[name] = value
0376
0377 def __get__(self, obj, type=None):
0378 if obj is None:
0379 return self
0380 return obj._default_values or {}
0381
0382 def __set__(self, obj, value):
0383 raise AttributeError(
0384 "The attribute %s cannot be set" % self.attr_name)
0385
0386 def ini_assignment(self, obj, value, name=None):
0387 assert name is None, "default values can't accept a name"
0388 all = []
0389 all_values = self.__get__(obj).items()
0390 all_values.sort()
0391 for name, value in all_values:
0392 if isinstance(value, (str, unicode)):
0393 value = [value]
0394 for subvalue in value:
0395 all.append('%s=%s\n' % (
0396 name, self.ini_fold(self.ini_repr(obj, subvalue))))
0397 return ''.join(all)
0398
0399class optconverter(opt):
0400
0401 misc_error_message = "%(error_type)s: %(message)s"
0402 converter_func = None
0403
0404 def convert(self, obj, value):
0405 try:
0406 return self.converter_func(value)
0407 except Exception, e:
0408 raise ParseValueError(
0409 self.misc_error_message % {
0410 'message': str(e),
0411 'error_type': e.__class__.__name__})
0412
0413 _converters = {}
0414 def get_converter(cls, name, converter_func):
0415 if cls._converters.has_key(id(converter_func)):
0416 return cls._converters[(name, id(converter_func))]
0417 else:
0418 converter = cls(name, converter_func=converter_func)
0419 cls._converters[(name, id(converter_func))] = converter
0420 return converter
0421 get_converter = classmethod(get_converter)
0422
0423class SchemaINIParser(iniparser.INIParser):
0424
0425 def __init__(self, schema):
0426 self.schema = schema
0427
0428 def new_section(self, section):
0429 self.parse_error("Schemas do not yet support sections")
0430
0431 def assignment(self, name, content):
0432 try:
0433 self.schema.set_config_value(name, content)
0434 except ParseValueError, e:
0435 self.parse_error(e.args[0])