Mercurial > p > roundup > code
changeset 2646:fd7b2fc1eb28
Config is now base class for all configurations...
...main Roundup configuration class is CoreConfig.
Builtin TRACKER_HOME renamed to HOME in Config and Options.
In CoreConfig, TRACKER_HOME is an alias for HOME.
Option.value2str(): added parameter 'current' -
stringify current Option value.
Option RDBMS_PORT defaults to None.
Config: added methods add_section, update_option, getopt (not implemented).
Config.save(): added warning about unset options at the top of the file.
new class UserConfig for inifile-driven configs.
CoreConfig: new attributes .ext and .detectors holding UserConfigs
for extensions and detectors, respectively.
| author | Alexander Smishlajev <a1s@users.sourceforge.net> |
|---|---|
| date | Tue, 27 Jul 2004 11:26:20 +0000 |
| parents | 8250c63c3963 |
| children | 8c46091c36d0 |
| files | roundup/configuration.py |
| diffstat | 1 files changed, 398 insertions(+), 149 deletions(-) [+] |
line wrap: on
line diff
--- a/roundup/configuration.py Tue Jul 27 11:11:03 2004 +0000 +++ b/roundup/configuration.py Tue Jul 27 11:26:20 2004 +0000 @@ -1,6 +1,6 @@ # Roundup Issue Tracker configuration support # -# $Id: configuration.py,v 1.13 2004-07-27 01:59:28 richard Exp $ +# $Id: configuration.py,v 1.14 2004-07-27 11:26:20 a1s Exp $ # __docformat__ = "restructuredtext" @@ -23,7 +23,7 @@ """Raised when configuration loading fails - Constructor parameters: path to the directory that was used as TRACKER_HOME + Constructor parameters: path to the directory that was used as HOME """ @@ -158,8 +158,14 @@ """ return str(value) - def value2str(self, value): - """Return 'value' argument converted to external representation""" + def value2str(self, value=NODEFAULT, current=0): + """Return 'value' argument converted to external representation + + If 'current' is True, use current option value. + + """ + if current: + value = self._value if value is NODEFAULT: return str(value) else: @@ -292,17 +298,17 @@ """File or directory path name - Paths may be either absolute or relative to the TRACKER_HOME. + Paths may be either absolute or relative to the HOME. """ - class_description = "The path may be either absolute" \ - " or relative to the tracker home." + class_description = "The path may be either absolute or relative\n" \ + " to the directory containig this config file." def get(self): _val = Option.get(self) if _val and not os.path.isabs(_val): - _val = os.path.join(self.config["TRACKER_HOME"], _val) + _val = os.path.join(self.config["HOME"], _val) return _val class FloatNumberOption(Option): @@ -451,7 +457,7 @@ (NullableOption, 'host', 'localhost', "Hostname that the Postgresql or MySQL database resides on.", ['MYSQL_DBHOST']), - (NullableOption, 'port', '5432', + (NullableOption, 'port', '', "Port number that the Postgresql or MySQL database resides on."), (NullableOption, 'user', 'roundup', "Postgresql or MySQL database user that Roundup should use.", @@ -548,51 +554,82 @@ )), ) -### Main class +### Configuration classes class Config: - """Roundup instance configuration. + """Base class for configuration objects. Configuration options may be accessed as attributes or items of instances of this class. All option names are uppercased. """ - # Config file names (in the TRACKER_HOME directory): - INI_FILE = "config.ini" # new style config file name - PYCONFIG = "config" # module name for old style configuration + # Config file name + INI_FILE = "config.ini" # Object attributes that should not be taken as common configuration # options in __setattr__ (most of them are initialized in constructor): - # builtin pseudo-option - tracker home directory - TRACKER_HOME = "." + # builtin pseudo-option - package home directory + HOME = "." # names of .ini file sections, in order sections = None + # section comments + section_descriptions = None # lists of option names for each section, in order section_options = None # mapping from option names and aliases to Option instances options = None - # logging engine - logging = rlog.BasicLogging() + + def __init__(self, home_dir=None, layout=None): + """Initialize confing instance - def __init__(self, tracker_home=None): + Parameters: + home_dir: + optional configuration directory. + If passed, load the config from that directory + after processing config layout (if any). + layout: + optional configuration layout, a sequence of + section definitions suitable for .add_section() + + """ # initialize option containers: self.sections = [] + self.section_descriptions = {} self.section_options = {} self.options = {} - # add options from the SETTINGS structure - for (_section, _options) in SETTINGS: - for _option_def in _options: - _class = _option_def[0] - _args = _option_def[1:] - _option = _class(self, _section, *_args) - self.add_option(_option) - # load the config if tracker_home given - if tracker_home is None: - self.init_logging() - else: - self.load(tracker_home) + # add options from the layout structure + if layout: + for section in layout: + self.add_section(*section) + if home_dir is not None: + self.load(home_dir) + + def add_section(self, section, options, description=None): + """Define new config section + + Parameters: + section - name of the config.ini section + options - a sequence of Option definitions. + Each Option definition is a sequence + containing class object and constructor + parameters, starting from the setting name: + setting, default, [description, [aliases]] + description - optional section comment + + Note: aliases should only exist in historical options + for backwards compatibility - new options should + *not* have aliases! + + """ + if description or not self.section_descriptions.has_key(section): + self.section_descriptions[section] = description + for option_def in options: + klass = option_def[0] + args = option_def[1:] + option = klass(self, section, *args) + self.add_option(option) def add_option(self, option): """Adopt a new Option object""" @@ -609,10 +646,312 @@ for _name in option.aliases: self.options[_name] = option + def update_option(self, name, klass, + default=NODEFAULT, description=None + ): + """Override behaviour of early created option. + + Parameters: + name: + option name + klass: + one of the Option classes + default: + optional default value for the option + description: + optional new description for the option + + Conversion from current option value to new class value + is done via string representation. + + This method may be used to attach some brains + to options autocreated by UserConfig. + + """ + # fetch current option + option = self._get_option(name) + # compute constructor parameters + if default is NODEFAULT: + default = option.default + if description is None: + description = option.description + value = option.value2str(current=1) + # resurrect the option + option = klass(self, option.section, option.setting, + default=default, description=description) + # apply the value + option.set(value) + # incorporate new option + del self[name] + self.add_option(option) + def reset(self): """Set all options to their default values""" for _option in self.items(): _option.reset() + + # This is a placeholder. TBD. + # Meant for commandline tools. + # Allows automatic creation of configuration files like this: + # roundup-server -p 8017 -u roundup --save-config + def getopt(self, args, **options): + """Apply options specified in command line arguments. + + Parameters: + args: + command line to parse (sys.argv[1:]) + options: + mapping from option names to command line option specs. + e.g. server_port="p:", server_user="u:" + Names are forced to lower case for commandline parsing + (long options) and to upper case to find config options. + Command line options accepting no value are assumed + to be binary and receive value 'yes'. + + Return value: same as for python standard getopt(), except that + processed options are removed from returned option list. + + """ + + # option and section locators (used in option access methods) + + def _get_option(self, name): + try: + return self.options[name] + except KeyError: + raise InvalidOptionError(name) + + def _get_section_options(self, name): + return self.section_options.setdefault(name, []) + + def _get_unset_options(self): + """Return options that need manual adjustments + + Return value is a dictionary where keys are section + names and values are lists of option names as they + appear in the config file. + + """ + need_set = {} + for option in self.items(): + if not option.isset(): + need_set.setdefault(option.section, []).append(option.setting) + return need_set + + # file operations + + def load_ini(self, home_dir, defaults=None): + """Set options from config.ini file in given home_dir + + Parameters: + home_dir: + config home directory + defaults: + optional dictionary of defaults for ConfigParser + + Note: if home_dir does not contain config.ini file, + no error is raised. Config will be reset to defaults. + + """ + # parse the file + config_defaults = {"HOME": home_dir} + if defaults: + config_defaults.update(defaults) + _config = ConfigParser.ConfigParser(defaults) + _config.read([os.path.join(home_dir, self.INI_FILE)]) + # .ini file loaded ok. set the options, starting from HOME + self.reset() + self.HOME = home_dir + for _option in self.items(): + _option.load_ini(_config) + + def load(self, home_dir): + """Load configuration settings from home_dir""" + self.load_ini(home_dir) + + def save(self, ini_file=None): + """Write current configuration to .ini file + + 'ini_file' argument, if passed, must be valid full path + to the file to write. If omitted, default file in current + HOME is created. + + If the file to write already exists, it is saved with '.bak' + extension. + + """ + if ini_file is None: + ini_file = os.path.join(self.HOME, self.INI_FILE) + _tmp_file = os.path.splitext(ini_file)[0] + _bak_file = _tmp_file + ".bak" + _tmp_file = _tmp_file + ".tmp" + _fp = file(_tmp_file, "wt") + _fp.write("# %s configuration file\n" % self["TRACKER_NAME"]) + _fp.write("# Autogenerated at %s\n" % time.asctime()) + need_set = self._get_unset_options() + if need_set: + _fp.write("\n# WARNING! Following options need adjustments:\n") + for section, options in need_set.items(): + _fp.write("# [%s]: %s\n" % (section, ", ".join(options))) + for _section in self.sections: + _fp.write("\n[%s]\n" % _section) + for _option in self._get_section_options(_section): + _fp.write("\n" + self.options[(_section, _option)].format()) + _fp.close() + if os.access(ini_file, os.F_OK): + if os.access(_bak_file, os.F_OK): + os.remove(_bak_file) + os.rename(ini_file, _bak_file) + os.rename(_tmp_file, ini_file) + + # container emulation + + def __len__(self): + return len(self.items()) + + def __getitem__(self, name): + if name == "HOME": + return self.HOME + else: + return self._get_option(name).get() + + def __setitem__(self, name, value): + if name == "HOME": + self.HOME = value + else: + self._get_option(name).set(value) + + def __delitem__(self, name): + _option = self._get_option(name) + _section = _option.section + _name = _option.setting + self._get_section_options(_section).remove(_name) + del self.options[(_section, _name)] + for _alias in _option.aliases: + del self.options[_alias] + + def items(self): + """Return the list of Option objects, in .ini file order + + Note that HOME is not included in this list + because it is builtin pseudo-option, not a real Option + object loaded from or saved to .ini file. + + """ + return [self.options[(_section, _name)] + for _section in self.sections + for _name in self._get_section_options(_section) + ] + + def keys(self): + """Return the list of "canonical" names of the options + + Unlike .items(), this list also includes HOME + + """ + return ["HOME"] + [_option.name for _option in self.items()] + + # .values() is not implemented because i am not sure what should be + # the values returned from this method: Option instances or config values? + + # attribute emulation + + def __setattr__(self, name, value): + if self.__dict__.has_key(name) or hasattr(self.__class__, name): + self.__dict__[name] = value + else: + self._get_option(name).set(value) + + # Note: __getattr__ is not symmetric to __setattr__: + # self.__dict__ lookup is done before calling this method + def __getattr__(self, name): + return self[name] + +class UserConfig(Config): + + """Configuration for user extensions. + + Instances of this class have no predefined configuration layout. + Options are created on the fly for each setting present in the + config file. + + """ + + def load_ini(self, home_dir, defaults=None): + """Load options from config.ini file in given home_dir + + Parameters: + home_dir: + config home directory + defaults: + optional dictionary of defaults for ConfigParser + + Options are automatically created as they are read + from the config file. + + Note: if home_dir does not contain config.ini file, + no error is raised. Config will be reset to defaults. + + """ + # parse the file + config_defaults = {"HOME": home_dir} + if defaults: + config_defaults.update(defaults) + config = ConfigParser.ConfigParser(defaults) + config.read([os.path.join(home_dir, self.INI_FILE)]) + # .ini file loaded ok. + self.HOME = home_dir + # see what options are already defined and add missing ones + preset = [(option.section, option.setting) for option in self.items()] + for section in config.sections(): + for name in config.options(section): + if (section, name) not in preset: + self.add_option(Option(self, section, name)) + # set the options + self.reset() + for option in self.items(): + option.load_ini(config) + +class CoreConfig(Config): + + """Roundup instance configuration. + + Core config has a predefined layout (see the SETTINGS structure), + support loading of old-style pythonic configurations and hold + three additional attributes: + logging: + instance logging engine, from standard python logging module + or minimalistic logger implemented in Roundup + detectors: + user-defined configuration for detectors + ext: + user-defined configuration for extensions + + """ + + # module name for old style configuration + PYCONFIG = "config" + # logging engine + logging = rlog.BasicLogging() + # user configs + ext = None + detectors = None + + def __init__(self, home_dir=None): + Config.__init__(self, home_dir, SETTINGS) + # load the config if home_dir given + if home_dir is None: + self.init_logging() + + # TODO: remove MAIL_PASSWORD if MAIL_USER is empty + #def _get_unset_options(self): + + def reset(self): + Config.reset(self) + if self.ext: + self.ext.reset() + if self.detectors: + self.detectors.reset() self.init_logging() def init_logging(self): @@ -634,58 +973,42 @@ _logging.setLevel(self["LOGGING_LEVEL"] or "ERROR") self.logging = _logging - # option and section locators (used in option access methods) - - def _get_option(self, name): - try: - return self.options[name] - except KeyError: - raise InvalidOptionError(name) - - def _get_section_options(self, name): - return self.section_options.setdefault(name, []) - - # file operations + def load(self, home_dir): + """Load configuration from path designated by home_dir argument""" + if os.path.isfile(os.path.join(home_dir, self.INI_FILE)): + self.load_ini(home_dir) + else: + self.load_pyconfig(home_dir) + self.init_logging() + self.ext = UserConfig(os.path.join(home_dir, "extensions")) + self.detectors = UserConfig(os.path.join(home_dir, "detectors")) - def load(self, tracker_home): - """Load configuration from path designated by tracker_home argument""" - if os.path.isfile(os.path.join(tracker_home, self.INI_FILE)): - self.load_ini(tracker_home) - else: - self.load_pyconfig(tracker_home) + def load_ini(self, home_dir, defaults=None): + """Set options from config.ini file in given home_dir directory""" + config_defaults = {"TRACKER_HOME": home_dir} + if defaults: + config_defaults.update(defaults) + Config.load_ini(self, home_dir, config_defaults) - def load_ini(self, tracker_home): - """Set options from config.ini file in given tracker_home directory""" - # parse the file - _config = ConfigParser.ConfigParser({"TRACKER_HOME": tracker_home}) - _config.read([os.path.join(tracker_home, self.INI_FILE)]) - # .ini file loaded ok. set the options, starting from TRACKER_HOME - self.reset() - self.TRACKER_HOME = tracker_home - for _option in self.items(): - _option.load_ini(_config) - self.init_logging() - - def load_pyconfig(self, tracker_home): - """Set options from config.py file in given tracker_home directory""" + def load_pyconfig(self, home_dir): + """Set options from config.py file in given home_dir directory""" # try to locate and import the module _mod_fp = None try: try: - _module = imp.find_module(self.PYCONFIG, [tracker_home]) + _module = imp.find_module(self.PYCONFIG, [home_dir]) _mod_fp = _module[0] _config = imp.load_module(self.PYCONFIG, *_module) except ImportError: - raise NoConfigError(tracker_home) + raise NoConfigError(home_dir) finally: if _mod_fp is not None: _mod_fp.close() - # module loaded ok. set the options, starting from TRACKER_HOME + # module loaded ok. set the options, starting from HOME self.reset() - self.TRACKER_HOME = tracker_home + self.HOME = home_dir for _option in self.items(): _option.load_pyconfig(_config) - self.init_logging() # backward compatibility: # SMTP login parameters were specified as a tuple in old style configs # convert them to new plain string options @@ -695,97 +1018,23 @@ if len(_mailuser) > 1: self.MAIL_PASSWORD = _mailuser[1] - def save(self, ini_file=None): - """Write current configuration to .ini file - - 'ini_file' argument, if passed, must be valid full path - to the file to write. If omitted, default file in current - TRACKER_HOME is created. - - If the file to write already exists, it is saved with '.bak' - extension. - - """ - if ini_file is None: - ini_file = os.path.join(self.TRACKER_HOME, self.INI_FILE) - _tmp_file = os.path.splitext(ini_file)[0] - _bak_file = _tmp_file + ".bak" - _tmp_file = _tmp_file + ".tmp" - _fp = file(_tmp_file, "wt") - _fp.write("# %s configuration file\n" % self["TRACKER_NAME"]) - _fp.write("# Autogenerated at %s\n" % time.asctime()) - for _section in self.sections: - _fp.write("\n[%s]\n" % _section) - for _option in self._get_section_options(_section): - _fp.write("\n" + self.options[(_section, _option)].format()) - _fp.close() - if os.access(ini_file, os.F_OK): - if os.access(_bak_file, os.F_OK): - os.remove(_bak_file) - os.rename(ini_file, _bak_file) - os.rename(_tmp_file, ini_file) - - # container emulation - - def __len__(self): - return len(self.items()) - + # in this config, HOME is also known as TRACKER_HOME def __getitem__(self, name): if name == "TRACKER_HOME": - return self.TRACKER_HOME + return self.HOME else: - return self._get_option(name).get() + return Config.__getitem__(self, name) def __setitem__(self, name, value): if name == "TRACKER_HOME": - self.TRACKER_HOME = value + self.HOME = value else: self._get_option(name).set(value) - def __delitem__(self, name): - _option = self._get_option(name) - _section = _option.section - _name = _option.setting - self._get_section_options(_section).remove(_name) - del self.options[(_section, _name)] - for _alias in _option.aliases: - del self.options[_alias] - - def items(self): - """Return the list of Option objects, in .ini file order - - Note that TRACKER_HOME is not included in this list - because it is builtin pseudo-option, not a real Option - object loaded from or saved to .ini file. - - """ - return [self.options[(_section, _name)] - for _section in self.sections - for _name in self._get_section_options(_section) - ] - - def keys(self): - """Return the list of "canonical" names of the options - - Unlike .items(), this list also includes TRACKER_HOME - - """ - return ["TRACKER_HOME"] + [_option.name for _option in self.items()] - - # .values() is not implemented because i am not sure what should be - # the values returned from this method: Option instances or config values? - - # attribute emulation - def __setattr__(self, name, value): - if self.__dict__.has_key(name) \ - or self.__class__.__dict__.has_key(name): - self.__dict__[name] = value + if name == "TRACKER_HOME": + self.__dict__["HOME"] = value else: - self._get_option(name).set(value) - - # Note: __getattr__ is not symmetric to __setattr__: - # self.__dict__ lookup is done before calling this method - __getattr__ = __getitem__ + Config.__setattr__(self, name, value) # vim: set et sts=4 sw=4 :
