comparison roundup/configuration.py @ 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 11811b313459
children 1df7d4a41da4
comparison
equal deleted inserted replaced
2645:8250c63c3963 2646:fd7b2fc1eb28
1 # Roundup Issue Tracker configuration support 1 # Roundup Issue Tracker configuration support
2 # 2 #
3 # $Id: configuration.py,v 1.13 2004-07-27 01:59:28 richard Exp $ 3 # $Id: configuration.py,v 1.14 2004-07-27 11:26:20 a1s Exp $
4 # 4 #
5 __docformat__ = "restructuredtext" 5 __docformat__ = "restructuredtext"
6 6
7 import imp 7 import imp
8 import os 8 import os
21 21
22 class NoConfigError(ConfigurationError): 22 class NoConfigError(ConfigurationError):
23 23
24 """Raised when configuration loading fails 24 """Raised when configuration loading fails
25 25
26 Constructor parameters: path to the directory that was used as TRACKER_HOME 26 Constructor parameters: path to the directory that was used as HOME
27 27
28 """ 28 """
29 29
30 def __str__(self): 30 def __str__(self):
31 return "No valid configuration files found in directory %s" \ 31 return "No valid configuration files found in directory %s" \
156 override this method, not the public .value2str(). 156 override this method, not the public .value2str().
157 157
158 """ 158 """
159 return str(value) 159 return str(value)
160 160
161 def value2str(self, value): 161 def value2str(self, value=NODEFAULT, current=0):
162 """Return 'value' argument converted to external representation""" 162 """Return 'value' argument converted to external representation
163
164 If 'current' is True, use current option value.
165
166 """
167 if current:
168 value = self._value
163 if value is NODEFAULT: 169 if value is NODEFAULT:
164 return str(value) 170 return str(value)
165 else: 171 else:
166 return self._value2str(value) 172 return self._value2str(value)
167 173
290 296
291 class FilePathOption(Option): 297 class FilePathOption(Option):
292 298
293 """File or directory path name 299 """File or directory path name
294 300
295 Paths may be either absolute or relative to the TRACKER_HOME. 301 Paths may be either absolute or relative to the HOME.
296 302
297 """ 303 """
298 304
299 class_description = "The path may be either absolute" \ 305 class_description = "The path may be either absolute or relative\n" \
300 " or relative to the tracker home." 306 " to the directory containig this config file."
301 307
302 def get(self): 308 def get(self):
303 _val = Option.get(self) 309 _val = Option.get(self)
304 if _val and not os.path.isabs(_val): 310 if _val and not os.path.isabs(_val):
305 _val = os.path.join(self.config["TRACKER_HOME"], _val) 311 _val = os.path.join(self.config["HOME"], _val)
306 return _val 312 return _val
307 313
308 class FloatNumberOption(Option): 314 class FloatNumberOption(Option):
309 315
310 """Floating point numbers""" 316 """Floating point numbers"""
449 "Name of the Postgresql or MySQL database to use.", 455 "Name of the Postgresql or MySQL database to use.",
450 ['MYSQL_DBNAME']), 456 ['MYSQL_DBNAME']),
451 (NullableOption, 'host', 'localhost', 457 (NullableOption, 'host', 'localhost',
452 "Hostname that the Postgresql or MySQL database resides on.", 458 "Hostname that the Postgresql or MySQL database resides on.",
453 ['MYSQL_DBHOST']), 459 ['MYSQL_DBHOST']),
454 (NullableOption, 'port', '5432', 460 (NullableOption, 'port', '',
455 "Port number that the Postgresql or MySQL database resides on."), 461 "Port number that the Postgresql or MySQL database resides on."),
456 (NullableOption, 'user', 'roundup', 462 (NullableOption, 'user', 'roundup',
457 "Postgresql or MySQL database user that Roundup should use.", 463 "Postgresql or MySQL database user that Roundup should use.",
458 ['MYSQL_DBUSER']), 464 ['MYSQL_DBUSER']),
459 (NullableOption, 'password', 'roundup', 465 (NullableOption, 'password', 'roundup',
546 "If 'no', they're never added to the nosy.\n", 552 "If 'no', they're never added to the nosy.\n",
547 ["ADD_RECIPIENTS_TO_NOSY"]), 553 ["ADD_RECIPIENTS_TO_NOSY"]),
548 )), 554 )),
549 ) 555 )
550 556
551 ### Main class 557 ### Configuration classes
552 558
553 class Config: 559 class Config:
554 560
555 """Roundup instance configuration. 561 """Base class for configuration objects.
556 562
557 Configuration options may be accessed as attributes or items 563 Configuration options may be accessed as attributes or items
558 of instances of this class. All option names are uppercased. 564 of instances of this class. All option names are uppercased.
559 565
560 """ 566 """
561 567
562 # Config file names (in the TRACKER_HOME directory): 568 # Config file name
563 INI_FILE = "config.ini" # new style config file name 569 INI_FILE = "config.ini"
564 PYCONFIG = "config" # module name for old style configuration
565 570
566 # Object attributes that should not be taken as common configuration 571 # Object attributes that should not be taken as common configuration
567 # options in __setattr__ (most of them are initialized in constructor): 572 # options in __setattr__ (most of them are initialized in constructor):
568 # builtin pseudo-option - tracker home directory 573 # builtin pseudo-option - package home directory
569 TRACKER_HOME = "." 574 HOME = "."
570 # names of .ini file sections, in order 575 # names of .ini file sections, in order
571 sections = None 576 sections = None
577 # section comments
578 section_descriptions = None
572 # lists of option names for each section, in order 579 # lists of option names for each section, in order
573 section_options = None 580 section_options = None
574 # mapping from option names and aliases to Option instances 581 # mapping from option names and aliases to Option instances
575 options = None 582 options = None
576 # logging engine 583
577 logging = rlog.BasicLogging() 584 def __init__(self, home_dir=None, layout=None):
578 585 """Initialize confing instance
579 def __init__(self, tracker_home=None): 586
587 Parameters:
588 home_dir:
589 optional configuration directory.
590 If passed, load the config from that directory
591 after processing config layout (if any).
592 layout:
593 optional configuration layout, a sequence of
594 section definitions suitable for .add_section()
595
596 """
580 # initialize option containers: 597 # initialize option containers:
581 self.sections = [] 598 self.sections = []
599 self.section_descriptions = {}
582 self.section_options = {} 600 self.section_options = {}
583 self.options = {} 601 self.options = {}
584 # add options from the SETTINGS structure 602 # add options from the layout structure
585 for (_section, _options) in SETTINGS: 603 if layout:
586 for _option_def in _options: 604 for section in layout:
587 _class = _option_def[0] 605 self.add_section(*section)
588 _args = _option_def[1:] 606 if home_dir is not None:
589 _option = _class(self, _section, *_args) 607 self.load(home_dir)
590 self.add_option(_option) 608
591 # load the config if tracker_home given 609 def add_section(self, section, options, description=None):
592 if tracker_home is None: 610 """Define new config section
593 self.init_logging() 611
594 else: 612 Parameters:
595 self.load(tracker_home) 613 section - name of the config.ini section
614 options - a sequence of Option definitions.
615 Each Option definition is a sequence
616 containing class object and constructor
617 parameters, starting from the setting name:
618 setting, default, [description, [aliases]]
619 description - optional section comment
620
621 Note: aliases should only exist in historical options
622 for backwards compatibility - new options should
623 *not* have aliases!
624
625 """
626 if description or not self.section_descriptions.has_key(section):
627 self.section_descriptions[section] = description
628 for option_def in options:
629 klass = option_def[0]
630 args = option_def[1:]
631 option = klass(self, section, *args)
632 self.add_option(option)
596 633
597 def add_option(self, option): 634 def add_option(self, option):
598 """Adopt a new Option object""" 635 """Adopt a new Option object"""
599 _section = option.section 636 _section = option.section
600 _name = option.setting 637 _name = option.setting
607 self.options[(_section, _name)] = option 644 self.options[(_section, _name)] = option
608 # make the option known under all of it's A.K.A.s 645 # make the option known under all of it's A.K.A.s
609 for _name in option.aliases: 646 for _name in option.aliases:
610 self.options[_name] = option 647 self.options[_name] = option
611 648
649 def update_option(self, name, klass,
650 default=NODEFAULT, description=None
651 ):
652 """Override behaviour of early created option.
653
654 Parameters:
655 name:
656 option name
657 klass:
658 one of the Option classes
659 default:
660 optional default value for the option
661 description:
662 optional new description for the option
663
664 Conversion from current option value to new class value
665 is done via string representation.
666
667 This method may be used to attach some brains
668 to options autocreated by UserConfig.
669
670 """
671 # fetch current option
672 option = self._get_option(name)
673 # compute constructor parameters
674 if default is NODEFAULT:
675 default = option.default
676 if description is None:
677 description = option.description
678 value = option.value2str(current=1)
679 # resurrect the option
680 option = klass(self, option.section, option.setting,
681 default=default, description=description)
682 # apply the value
683 option.set(value)
684 # incorporate new option
685 del self[name]
686 self.add_option(option)
687
612 def reset(self): 688 def reset(self):
613 """Set all options to their default values""" 689 """Set all options to their default values"""
614 for _option in self.items(): 690 for _option in self.items():
615 _option.reset() 691 _option.reset()
692
693 # This is a placeholder. TBD.
694 # Meant for commandline tools.
695 # Allows automatic creation of configuration files like this:
696 # roundup-server -p 8017 -u roundup --save-config
697 def getopt(self, args, **options):
698 """Apply options specified in command line arguments.
699
700 Parameters:
701 args:
702 command line to parse (sys.argv[1:])
703 options:
704 mapping from option names to command line option specs.
705 e.g. server_port="p:", server_user="u:"
706 Names are forced to lower case for commandline parsing
707 (long options) and to upper case to find config options.
708 Command line options accepting no value are assumed
709 to be binary and receive value 'yes'.
710
711 Return value: same as for python standard getopt(), except that
712 processed options are removed from returned option list.
713
714 """
715
716 # option and section locators (used in option access methods)
717
718 def _get_option(self, name):
719 try:
720 return self.options[name]
721 except KeyError:
722 raise InvalidOptionError(name)
723
724 def _get_section_options(self, name):
725 return self.section_options.setdefault(name, [])
726
727 def _get_unset_options(self):
728 """Return options that need manual adjustments
729
730 Return value is a dictionary where keys are section
731 names and values are lists of option names as they
732 appear in the config file.
733
734 """
735 need_set = {}
736 for option in self.items():
737 if not option.isset():
738 need_set.setdefault(option.section, []).append(option.setting)
739 return need_set
740
741 # file operations
742
743 def load_ini(self, home_dir, defaults=None):
744 """Set options from config.ini file in given home_dir
745
746 Parameters:
747 home_dir:
748 config home directory
749 defaults:
750 optional dictionary of defaults for ConfigParser
751
752 Note: if home_dir does not contain config.ini file,
753 no error is raised. Config will be reset to defaults.
754
755 """
756 # parse the file
757 config_defaults = {"HOME": home_dir}
758 if defaults:
759 config_defaults.update(defaults)
760 _config = ConfigParser.ConfigParser(defaults)
761 _config.read([os.path.join(home_dir, self.INI_FILE)])
762 # .ini file loaded ok. set the options, starting from HOME
763 self.reset()
764 self.HOME = home_dir
765 for _option in self.items():
766 _option.load_ini(_config)
767
768 def load(self, home_dir):
769 """Load configuration settings from home_dir"""
770 self.load_ini(home_dir)
771
772 def save(self, ini_file=None):
773 """Write current configuration to .ini file
774
775 'ini_file' argument, if passed, must be valid full path
776 to the file to write. If omitted, default file in current
777 HOME is created.
778
779 If the file to write already exists, it is saved with '.bak'
780 extension.
781
782 """
783 if ini_file is None:
784 ini_file = os.path.join(self.HOME, self.INI_FILE)
785 _tmp_file = os.path.splitext(ini_file)[0]
786 _bak_file = _tmp_file + ".bak"
787 _tmp_file = _tmp_file + ".tmp"
788 _fp = file(_tmp_file, "wt")
789 _fp.write("# %s configuration file\n" % self["TRACKER_NAME"])
790 _fp.write("# Autogenerated at %s\n" % time.asctime())
791 need_set = self._get_unset_options()
792 if need_set:
793 _fp.write("\n# WARNING! Following options need adjustments:\n")
794 for section, options in need_set.items():
795 _fp.write("# [%s]: %s\n" % (section, ", ".join(options)))
796 for _section in self.sections:
797 _fp.write("\n[%s]\n" % _section)
798 for _option in self._get_section_options(_section):
799 _fp.write("\n" + self.options[(_section, _option)].format())
800 _fp.close()
801 if os.access(ini_file, os.F_OK):
802 if os.access(_bak_file, os.F_OK):
803 os.remove(_bak_file)
804 os.rename(ini_file, _bak_file)
805 os.rename(_tmp_file, ini_file)
806
807 # container emulation
808
809 def __len__(self):
810 return len(self.items())
811
812 def __getitem__(self, name):
813 if name == "HOME":
814 return self.HOME
815 else:
816 return self._get_option(name).get()
817
818 def __setitem__(self, name, value):
819 if name == "HOME":
820 self.HOME = value
821 else:
822 self._get_option(name).set(value)
823
824 def __delitem__(self, name):
825 _option = self._get_option(name)
826 _section = _option.section
827 _name = _option.setting
828 self._get_section_options(_section).remove(_name)
829 del self.options[(_section, _name)]
830 for _alias in _option.aliases:
831 del self.options[_alias]
832
833 def items(self):
834 """Return the list of Option objects, in .ini file order
835
836 Note that HOME is not included in this list
837 because it is builtin pseudo-option, not a real Option
838 object loaded from or saved to .ini file.
839
840 """
841 return [self.options[(_section, _name)]
842 for _section in self.sections
843 for _name in self._get_section_options(_section)
844 ]
845
846 def keys(self):
847 """Return the list of "canonical" names of the options
848
849 Unlike .items(), this list also includes HOME
850
851 """
852 return ["HOME"] + [_option.name for _option in self.items()]
853
854 # .values() is not implemented because i am not sure what should be
855 # the values returned from this method: Option instances or config values?
856
857 # attribute emulation
858
859 def __setattr__(self, name, value):
860 if self.__dict__.has_key(name) or hasattr(self.__class__, name):
861 self.__dict__[name] = value
862 else:
863 self._get_option(name).set(value)
864
865 # Note: __getattr__ is not symmetric to __setattr__:
866 # self.__dict__ lookup is done before calling this method
867 def __getattr__(self, name):
868 return self[name]
869
870 class UserConfig(Config):
871
872 """Configuration for user extensions.
873
874 Instances of this class have no predefined configuration layout.
875 Options are created on the fly for each setting present in the
876 config file.
877
878 """
879
880 def load_ini(self, home_dir, defaults=None):
881 """Load options from config.ini file in given home_dir
882
883 Parameters:
884 home_dir:
885 config home directory
886 defaults:
887 optional dictionary of defaults for ConfigParser
888
889 Options are automatically created as they are read
890 from the config file.
891
892 Note: if home_dir does not contain config.ini file,
893 no error is raised. Config will be reset to defaults.
894
895 """
896 # parse the file
897 config_defaults = {"HOME": home_dir}
898 if defaults:
899 config_defaults.update(defaults)
900 config = ConfigParser.ConfigParser(defaults)
901 config.read([os.path.join(home_dir, self.INI_FILE)])
902 # .ini file loaded ok.
903 self.HOME = home_dir
904 # see what options are already defined and add missing ones
905 preset = [(option.section, option.setting) for option in self.items()]
906 for section in config.sections():
907 for name in config.options(section):
908 if (section, name) not in preset:
909 self.add_option(Option(self, section, name))
910 # set the options
911 self.reset()
912 for option in self.items():
913 option.load_ini(config)
914
915 class CoreConfig(Config):
916
917 """Roundup instance configuration.
918
919 Core config has a predefined layout (see the SETTINGS structure),
920 support loading of old-style pythonic configurations and hold
921 three additional attributes:
922 logging:
923 instance logging engine, from standard python logging module
924 or minimalistic logger implemented in Roundup
925 detectors:
926 user-defined configuration for detectors
927 ext:
928 user-defined configuration for extensions
929
930 """
931
932 # module name for old style configuration
933 PYCONFIG = "config"
934 # logging engine
935 logging = rlog.BasicLogging()
936 # user configs
937 ext = None
938 detectors = None
939
940 def __init__(self, home_dir=None):
941 Config.__init__(self, home_dir, SETTINGS)
942 # load the config if home_dir given
943 if home_dir is None:
944 self.init_logging()
945
946 # TODO: remove MAIL_PASSWORD if MAIL_USER is empty
947 #def _get_unset_options(self):
948
949 def reset(self):
950 Config.reset(self)
951 if self.ext:
952 self.ext.reset()
953 if self.detectors:
954 self.detectors.reset()
616 self.init_logging() 955 self.init_logging()
617 956
618 def init_logging(self): 957 def init_logging(self):
619 _file = self["LOGGING_CONFIG"] 958 _file = self["LOGGING_CONFIG"]
620 if _file and os.path.isfile(_file): 959 if _file and os.path.isfile(_file):
632 if _file: 971 if _file:
633 _logging.setFile(_file) 972 _logging.setFile(_file)
634 _logging.setLevel(self["LOGGING_LEVEL"] or "ERROR") 973 _logging.setLevel(self["LOGGING_LEVEL"] or "ERROR")
635 self.logging = _logging 974 self.logging = _logging
636 975
637 # option and section locators (used in option access methods) 976 def load(self, home_dir):
638 977 """Load configuration from path designated by home_dir argument"""
639 def _get_option(self, name): 978 if os.path.isfile(os.path.join(home_dir, self.INI_FILE)):
640 try: 979 self.load_ini(home_dir)
641 return self.options[name] 980 else:
642 except KeyError: 981 self.load_pyconfig(home_dir)
643 raise InvalidOptionError(name)
644
645 def _get_section_options(self, name):
646 return self.section_options.setdefault(name, [])
647
648 # file operations
649
650 def load(self, tracker_home):
651 """Load configuration from path designated by tracker_home argument"""
652 if os.path.isfile(os.path.join(tracker_home, self.INI_FILE)):
653 self.load_ini(tracker_home)
654 else:
655 self.load_pyconfig(tracker_home)
656
657 def load_ini(self, tracker_home):
658 """Set options from config.ini file in given tracker_home directory"""
659 # parse the file
660 _config = ConfigParser.ConfigParser({"TRACKER_HOME": tracker_home})
661 _config.read([os.path.join(tracker_home, self.INI_FILE)])
662 # .ini file loaded ok. set the options, starting from TRACKER_HOME
663 self.reset()
664 self.TRACKER_HOME = tracker_home
665 for _option in self.items():
666 _option.load_ini(_config)
667 self.init_logging() 982 self.init_logging()
668 983 self.ext = UserConfig(os.path.join(home_dir, "extensions"))
669 def load_pyconfig(self, tracker_home): 984 self.detectors = UserConfig(os.path.join(home_dir, "detectors"))
670 """Set options from config.py file in given tracker_home directory""" 985
986 def load_ini(self, home_dir, defaults=None):
987 """Set options from config.ini file in given home_dir directory"""
988 config_defaults = {"TRACKER_HOME": home_dir}
989 if defaults:
990 config_defaults.update(defaults)
991 Config.load_ini(self, home_dir, config_defaults)
992
993 def load_pyconfig(self, home_dir):
994 """Set options from config.py file in given home_dir directory"""
671 # try to locate and import the module 995 # try to locate and import the module
672 _mod_fp = None 996 _mod_fp = None
673 try: 997 try:
674 try: 998 try:
675 _module = imp.find_module(self.PYCONFIG, [tracker_home]) 999 _module = imp.find_module(self.PYCONFIG, [home_dir])
676 _mod_fp = _module[0] 1000 _mod_fp = _module[0]
677 _config = imp.load_module(self.PYCONFIG, *_module) 1001 _config = imp.load_module(self.PYCONFIG, *_module)
678 except ImportError: 1002 except ImportError:
679 raise NoConfigError(tracker_home) 1003 raise NoConfigError(home_dir)
680 finally: 1004 finally:
681 if _mod_fp is not None: 1005 if _mod_fp is not None:
682 _mod_fp.close() 1006 _mod_fp.close()
683 # module loaded ok. set the options, starting from TRACKER_HOME 1007 # module loaded ok. set the options, starting from HOME
684 self.reset() 1008 self.reset()
685 self.TRACKER_HOME = tracker_home 1009 self.HOME = home_dir
686 for _option in self.items(): 1010 for _option in self.items():
687 _option.load_pyconfig(_config) 1011 _option.load_pyconfig(_config)
688 self.init_logging()
689 # backward compatibility: 1012 # backward compatibility:
690 # SMTP login parameters were specified as a tuple in old style configs 1013 # SMTP login parameters were specified as a tuple in old style configs
691 # convert them to new plain string options 1014 # convert them to new plain string options
692 _mailuser = getattr(_config, "MAILUSER", ()) 1015 _mailuser = getattr(_config, "MAILUSER", ())
693 if len(_mailuser) > 0: 1016 if len(_mailuser) > 0:
694 self.MAIL_USERNAME = _mailuser[0] 1017 self.MAIL_USERNAME = _mailuser[0]
695 if len(_mailuser) > 1: 1018 if len(_mailuser) > 1:
696 self.MAIL_PASSWORD = _mailuser[1] 1019 self.MAIL_PASSWORD = _mailuser[1]
697 1020
698 def save(self, ini_file=None): 1021 # in this config, HOME is also known as TRACKER_HOME
699 """Write current configuration to .ini file
700
701 'ini_file' argument, if passed, must be valid full path
702 to the file to write. If omitted, default file in current
703 TRACKER_HOME is created.
704
705 If the file to write already exists, it is saved with '.bak'
706 extension.
707
708 """
709 if ini_file is None:
710 ini_file = os.path.join(self.TRACKER_HOME, self.INI_FILE)
711 _tmp_file = os.path.splitext(ini_file)[0]
712 _bak_file = _tmp_file + ".bak"
713 _tmp_file = _tmp_file + ".tmp"
714 _fp = file(_tmp_file, "wt")
715 _fp.write("# %s configuration file\n" % self["TRACKER_NAME"])
716 _fp.write("# Autogenerated at %s\n" % time.asctime())
717 for _section in self.sections:
718 _fp.write("\n[%s]\n" % _section)
719 for _option in self._get_section_options(_section):
720 _fp.write("\n" + self.options[(_section, _option)].format())
721 _fp.close()
722 if os.access(ini_file, os.F_OK):
723 if os.access(_bak_file, os.F_OK):
724 os.remove(_bak_file)
725 os.rename(ini_file, _bak_file)
726 os.rename(_tmp_file, ini_file)
727
728 # container emulation
729
730 def __len__(self):
731 return len(self.items())
732
733 def __getitem__(self, name): 1022 def __getitem__(self, name):
734 if name == "TRACKER_HOME": 1023 if name == "TRACKER_HOME":
735 return self.TRACKER_HOME 1024 return self.HOME
736 else: 1025 else:
737 return self._get_option(name).get() 1026 return Config.__getitem__(self, name)
738 1027
739 def __setitem__(self, name, value): 1028 def __setitem__(self, name, value):
740 if name == "TRACKER_HOME": 1029 if name == "TRACKER_HOME":
741 self.TRACKER_HOME = value 1030 self.HOME = value
742 else: 1031 else:
743 self._get_option(name).set(value) 1032 self._get_option(name).set(value)
744 1033
745 def __delitem__(self, name):
746 _option = self._get_option(name)
747 _section = _option.section
748 _name = _option.setting
749 self._get_section_options(_section).remove(_name)
750 del self.options[(_section, _name)]
751 for _alias in _option.aliases:
752 del self.options[_alias]
753
754 def items(self):
755 """Return the list of Option objects, in .ini file order
756
757 Note that TRACKER_HOME is not included in this list
758 because it is builtin pseudo-option, not a real Option
759 object loaded from or saved to .ini file.
760
761 """
762 return [self.options[(_section, _name)]
763 for _section in self.sections
764 for _name in self._get_section_options(_section)
765 ]
766
767 def keys(self):
768 """Return the list of "canonical" names of the options
769
770 Unlike .items(), this list also includes TRACKER_HOME
771
772 """
773 return ["TRACKER_HOME"] + [_option.name for _option in self.items()]
774
775 # .values() is not implemented because i am not sure what should be
776 # the values returned from this method: Option instances or config values?
777
778 # attribute emulation
779
780 def __setattr__(self, name, value): 1034 def __setattr__(self, name, value):
781 if self.__dict__.has_key(name) \ 1035 if name == "TRACKER_HOME":
782 or self.__class__.__dict__.has_key(name): 1036 self.__dict__["HOME"] = value
783 self.__dict__[name] = value 1037 else:
784 else: 1038 Config.__setattr__(self, name, value)
785 self._get_option(name).set(value)
786
787 # Note: __getattr__ is not symmetric to __setattr__:
788 # self.__dict__ lookup is done before calling this method
789 __getattr__ = __getitem__
790 1039
791 # vim: set et sts=4 sw=4 : 1040 # vim: set et sts=4 sw=4 :

Roundup Issue Tracker: http://roundup-tracker.org/