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        # We don't initialize INISchema itself:
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        # @@: We should look for None-ified options, and remove
0116        # them from cls._config_names
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            # We use a list so that the descriptor behavior doesn't
0126            # apply here:
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        # @@: default values won't show up here
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])