Ticket #310: config.py

File config.py, 8.3 KB (added by aspidiets@…, 8 years ago)

Drop-in replacement for FIFE's settings module

Line 
1#   This file is part of PARPG.
2#
3#   PARPG is free software: you can redistribute it and/or modify
4#   it under the terms of the GNU General Public License as published by
5#   the Free Software Foundation, either version 3 of the License, or
6#   (at your option) any later version.
7#
8#   PARPG is distributed in the hope that it will be useful,
9#   but WITHOUT ANY WARRANTY; without even the implied warranty of
10#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11#   GNU General Public License for more details.
12#
13#   You should have received a copy of the GNU General Public License
14#   along with PARPG.  If not, see <http://www.gnu.org/licenses/>.
15
16""" Provides a class used for reading and writing various configurable options
17    throughout the game
18
19    This class produces an INI formated configuration file as opposed to an XML
20    formatted one. The reason that python's built-in ConfigurationParser isn't
21    sufficient is because comments aren't preserved when writing a configuration
22    file, the order in which the options are written isn't preserved, and the
23    interface used with this class is arguably more convenient that
24    ConfigParser's.
25"""
26class Section(object):
27    """ An object that represents a section in a configuration file.
28
29        Options can be added to a section by simply assigning a value to an
30            attribute:
31                section.foo = baz
32            would produce:
33                [section]
34                foo = baz
35            in the configuration file. Options that do not exist on assignment
36            are created dynamcially.
37    """
38    def __init__(self, name):
39        """ Initialize a new section.
40
41            @param name: name of the section. In the INI file, sections are surrounded
42                by brackets ([name])
43            @type name: string
44        """
45        self.name = name
46
47    def __setattr__(self, option, value):
48        """ Assign a value to an option, converting types when appropriate.
49
50            @param option: name of the option to assign a value to.
51            @type option: string
52            @param value: value to be assigned to the option.
53            @type value: int, float, string, boolean, or list
54        """
55        value = str(value)
56        if value.startswith('[') and value.endswith(']'):
57            value = [item.strip() for item in value[1:-1].split(',')]
58        elif value.lower() == 'true':
59            value = True
60        elif value.lower() == 'false':
61            value = False
62        elif value.isdigit():
63            value = int(value)
64        else:
65            try:
66                value = float(value)
67            except ValueError:
68                # leave as string
69                pass
70
71        self.__dict__[option] = value
72
73    def __getattr__(self, option):
74        """ Retrieve the value of the requested option
75            @param option: name of the option whose value is being requested
76        """
77        return self.__dict__[option]
78
79    def options(self):
80        """ Returns a dictionary of existing options """
81        options = self.__dict__
82        # get rid of properties that aren't actually options
83        if options.has_key('name'):
84            options.pop('name')
85
86        return options
87
88#TODO: remember config filenames
89class Config(object):
90    """ An object that represents a configuration file, its sectons,
91        and the options defined within those sections.
92    """
93    def __init__(self, *filenames):
94        """ initializes a new configuration object.
95
96            @param filenames: Either a string representing a filename or a list
97                of such strings. If a single string is given, configuration
98                sections and options are simply read from it. If a list is given,
99                each file is parsed sequentially with the next file's options
100                taking precedence over the previous one's. Consider:
101                    files = ['foo.cfg' ,'bar.cfg']
102                    config = Config(files)
103                First, foo.cfg is read, then, if similar options in bar.cfg exist,
104                they overwrite the ones previously set by foo.cfg.
105            @type filenames: either a string or list
106            @ivar config_file: Python object representing the configuration
107                file. Its purpose is to preserve the order of each section and
108                its options on read and write.
109            @type config_file: list
110        """
111        self.config_file = ''
112
113        if hasattr(filenames, 'split'):
114            self.read(filenames)
115        else:
116            for filename in filenames:
117                self.read(filename)
118
119    def __getattr__(self, name):
120        """ Returns a Section object to be used for assignment, creating one
121            if it doesn't exist.
122
123            @param name: name of section to be retrieved
124            @type name: string
125        """
126        if not self.__dict__.has_key(name):
127            setattr(self, name, Section(name))
128
129        return getattr(self, name)
130
131    def read(self, filename):
132        """ Reads a configuration file and populates the configuration object
133            with its sections and options.
134
135            @param filename: name of file to be parsed.
136            @type filename: string
137        """
138        section = None
139        try:
140            self.config_file = open(filename, 'r').readlines()
141        except IOError:
142            pass
143
144        for line in self.config_file:
145            if line.startswith('#') or line.strip() == '':
146                continue
147            elif line.startswith('[') and line.endswith(']\n'):
148                getattr(self, line[1:-2])
149                section = line[1:-2]
150            else:
151                option, value = [item.strip()
152                                 for item in line.split('=', 1)]
153
154                setattr(getattr(self, section), option, value)
155
156    def write(self, filename):
157        """ Writes a configuration file based on the configuration object's
158            sections and options
159
160            @param filename: name of file to save to
161            @type filename: string
162        """
163        for section in self.sections():
164            if '[{0}]\n'.format(section) not in self.config_file:
165                self.config_file.append('\n[{0}]\n'.format(section))
166                for option, value in getattr(self, section).options().iteritems():
167                    template = '{0} = {1}\n'.format(option, value)
168                    self.config_file.append(template)
169            else:
170                start_of_section = (self.config_file
171                                        .index('[{0}]\n'.format(section)) + 1)
172                for option, value in getattr(self, 
173                                             section).options().iteritems():
174
175                    if hasattr(value, 'sort'):
176                        value = '[{0}]'.format(', '.join(value))
177
178                    new_option = False
179                    template = '{0} = {1}\n'.format(option, value)
180                    for index, line in enumerate(self.config_file[:]):
181                        if option in line:
182                            new_option = False
183                            self.config_file[index] = template
184                            break
185                        else:
186                            new_option = True
187                    if new_option:
188                        self.config_file.insert(start_of_section, template)
189
190        with open(filename, 'w') as out_stream:
191            for line in self.config_file:
192                out_stream.write(line)
193
194    def sections(self):
195        """ Returns a list of existing sections"""
196        sections = self.__dict__.keys()
197        sections.pop(sections.index('config_file'))
198        return sections
199
200    def get(self, section, option):
201        """ Returns the value of the requested option located in the
202            requested section.
203
204            This method is provided strictly for backwards                   
205            compatibility be deprecated in future versions.
206 
207            @param section: the section in which the option is located in
208            @type section: string
209            @param option: the option being requested
210            @type section: string
211        """
212        return getattr(getattr(self, section), option)