"""A module supporting parsing and access to configuration files. The config module is designed to provide an easy way to place simple values within a configuation file yet access them easily from within your code. Here is a sample of how it could be used: import config def connect_via_http(): host = config.myApp.socket.machine port = int(config.myApp.socket.port) return httplib.HTTPConnection(host, port) The config module can read config files in Microsoft's ini file format, java's properties file format, or its own python config format -- these can even be mixed. Once read in, the configuration data is treated as if it were a tree of values, where the values are strings (either unicode or str) and are labled with "dot-separated-strings". The "dot-separated-strings" are something like "my_app.maxUsers" or "my_app.lab_settings.laser.microjules". Formally, these lables may consist of any string (either str or unicode) which is broken into components by splitting on '.' characters. The most basic methods of obtaining a value are to use config.values or the function config.get: x = config.values['my_app.foo.bar.baz'] x = config.get('my_app.foo.bar.baz') However, as long as the components of the path are valid Python identifiers, there is a more convenient attribute syntax available: x = config.my_app.foo.bar.baz And attribute syntax can be freely mixed with indexing: x = config.my_app['foo.bar'].baz If you attempt to access a value which has not been set, you will get a ConfigValueUndefinedException. The get() method provides a way to provide a default value instead: x = config.get('my_app.foo.bar.baz', default='123') The value returned will always be a str (or unicode) subclass. If you want to be able to configure other sorts of values like dates or numbers, simply convert from the string: x = int(config.my_app.number_of_users) x = time.strptime(config.my_app.expire_date) RECOMENDED USAGE: Key lengths: The config module tries to be quite lax in what keys it permits -- any string whatsoever is a legal key, including things like empty strings. However, it is probably wisest to stick with something simple, like valid python identifiers -- this avoids confusion and as a side effect allows the use of highly-readable "attribute syntax". In java properties files, the keys tend to be quite deep: "ejs.console.serious.event.poll.interval". While this works fine, we think it may be more pythonic to use flatter hierarchies: "ejsconsole.event_poll_interval". However, while a single-level hierarchy would work (ie: "event_poll_interval"), we recomend having a top level which is named for your application, package, or framework. This makes it easier for at reader to tell what a setting is related to, reduces the chance of name collisions, and makes it easier for large projects that combine many different components. On mixing str and unicode: Both keys and values can be either of type str or of type unicode. It is probably advisable for all of the config files to be either str or unicode, and not mix them (both keys and values will take their type from the type returned by iter(input)). However, mixing unicode and str will work so long as the str contain only valid ascii characters. """ import types # XXX - need to address threading issues class ConfigException(Exception): "This is a superclass for all exceptions defined in the config module." pass class ConfigValueUndefinedException(KeyError, ConfigException): """The exception raised when an attempt is made to obtain a config setting which has never been set.""" def __init__(self, path_tuple): KeyError.__init__(self, '%s not found' % '.'.join(path_tuple)) class ConfigKeyMustBeStringException(KeyError, ConfigException): """The exception raised when attempting to index a config node by something other than a string.""" def __init__(self): KeyError.__init__(self, 'Config path components must be str or unicode') class InvalidConfigValueType(TypeError, ConfigException): """The exception raised when a value is provided which is neither str nor unicode.""" def __init__(self): TypeError.__init__(self, "Values in the config module must be str or unicode.") class IntOrStringRequiredException(TypeError, ConfigException): """The exception raised when indexing an object which is behaving both as a config tree node and as a str.""" def __init__(self): TypeError.__init__(self, "Config node must be indexed by name (to find sub-nodes) " "or by integer (normal string indexing).") class ConfigNode(object): def __init__(self, path): self.path = path def __str__(self): value = data[self.path] if value is None: raise ConfigValueUndefinedException(self.path) else: return str(value) def __unicode__(self): value = data[self.path] if value is None: raise ConfigValueUndefinedException(self.path) else: return unicode(value) def __getitem__(self, name): if not isinstance(name, basestring): raise ConfigKeyMustBeStringException() new_path = self.path + tuple(name.split('.')) if new_path in data: return _make_new_node(new_path) else: raise ConfigValueUndefinedException(new_path) __getattr__ = __getitem__ def subconfig(self, deep=False): """Returns an iterator of the keys to immediate child nodes, or if deep=True to the keys for all decendent nodes. FIXME: These docs are not quite right. Fix. Returns rest of key, not key. """ my_path = self.path my_path_len = len(my_path) if deep: for key, value in data.iteritems(): if value is not None and len(key) > my_path_len and key[:my_path_len] == my_path: rest_of_path = key[my_path_len:] yield '.'.join(rest_of_path) else: desired_path_len = my_path_len + 1 for key in data.iterkeys(): if len(key) == desired_path_len and key[:my_path_len] == my_path: next_path_step = key[my_path_len] yield next_path_step class ConfigNodeAsStr(str, ConfigNode): def __new__(cls, value, path): return str.__new__(cls, value) def __init__(self, value, path): ConfigNode.__init__(self, path) def __getitem__(self, key): if isinstance(key, (int, long)): return str.__getitem__(self, key) elif isinstance(key, basestring): return ConfigNode.__getitem__(self, key) else: raise IntOrStringRequiredException() class ConfigNodeAsUnicode(unicode, ConfigNode): def __new__(cls, value, path): return unicode.__new__(cls, value) def __init__(self, value, path): ConfigNode.__init__(self, path) def __getitem__(self, key): if isinstance(key, (int, long)): return unicode.__getitem__(self, key) elif isinstance(key, basestring): return ConfigNode.__getitem__(self, key) else: raise IntOrStringRequiredException() def _make_new_node(path): value = data[path] if value is None: return ConfigNode(path) elif isinstance(value, str): return ConfigNodeAsStr(value, path) elif isinstance(value, unicode): return ConfigNodeAsUnicode(value, path) else: # FIXME: Handle this problem better raise Exception("Internal error: had %r in data" % value) """data uses the tuple form a paths as its keys, and uses strings as the values for leaf nodes; None as the value for intermediate nodes with no value of their own.""" data = {} values = ConfigNode(()) def set_value(key, value): if not isinstance(value, (str, unicode, types.NoneType)): raise InvalidConfigValueType() path = tuple(key.split('.')) data[path] = value if len(path) == 1: _add_top_level_name(path) else: for i in range(1, len(path)): sub_path = path[0:i] if sub_path not in data: data[sub_path] = None if len(sub_path) == 1: _add_top_level_name(sub_path) def set_values(d): """Initialize several values at once. This is passed a dict where the keys are dot-separated strings and the values are strings, and it sets these config values.""" for key, value in d.iteritems(): set_value(key, value) def clear_all(): """Clears all values from config. This is occasionally useful in applications (if immediately followed by reloading the config files), and is recomended for use in the tearDown() method of any unittests that set config values.""" data.clear() for name in _top_level_names_added: delattr(config_module, name) del _top_level_names_added[:] _top_level_names_added = [] def _add_top_level_name(path): assert len(path) == 1 name = path[0] if not hasattr(config_module, name): setattr(config_module, name, _make_new_node(path)) _top_level_names_added.append(name) _unique_object_for_get = object() def get(key, default=_unique_object_for_get): try: key_tuple = tuple(key.split('.')) return data[ key_tuple ] except KeyError: if default is _unique_object_for_get: raise ConfigValueUndefinedException(key_tuple) else: return default #def load_ini(lines) # """Reads a file in Microsoft's ini format. lines should be either a # file open for reading or a list of lines.""" # Do this last to minimize circular import issues import config as config_module