Mercurial > parpg-core
comparison src/parpg/settings.py @ 0:1fd2201f5c36
Initial commit of parpg-core.
author | M. George Hansen <technopolitica@gmail.com> |
---|---|
date | Sat, 14 May 2011 01:12:35 -0700 |
parents | |
children | 4706e0194af3 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:1fd2201f5c36 |
---|---|
1 #!/usr/bin/env python2 | |
2 | |
3 # Copyright (C) 2011 Edwin Marshall <emarshall85@gmail.com> | |
4 | |
5 # This file is part of PARPG. | |
6 # | |
7 # PARPG is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 # | |
12 # PARPG is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU General Public License for more details. | |
16 # | |
17 # You should have received a copy of the GNU General Public License | |
18 # along with PARPG. If not, see <http://www.gnu.org/licenses/>. | |
19 | |
20 """ Provides a class used for reading and writing various configurable options | |
21 throughout the game | |
22 | |
23 This class produces an INI formated settings file as opposed to an XML | |
24 formatted one. The reason that python's built-in ConfigurationParser isn't | |
25 sufficient is because comments aren't preserved when writing a settings | |
26 file, the order in which the options are written isn't preserved, and the | |
27 interface used with this class is arguably more convenient that | |
28 ConfigParser's. | |
29 | |
30 Default Settings may be generated by envoking this module from the | |
31 command line: | |
32 python -m settings.py [system] [data_directory] | |
33 | |
34 where [system] is one of local, windows, or linux (mac coming soon), | |
35 and data_directory is the base path for the data files to be loaded. | |
36 | |
37 Both [system] and [data_directory] are option. If omitted, both | |
38 default to whichever what is reasonable based on the system settings.py | |
39 is run on | |
40 """ | |
41 | |
42 import os | |
43 import sys | |
44 import platform | |
45 | |
46 #TODO: add logging to replace print statements | |
47 class Section(object): | |
48 """ An object that represents a section in a settings file. | |
49 | |
50 Options can be added to a section by simply assigning a value to an | |
51 attribute: | |
52 section.foo = baz | |
53 would produce: | |
54 [section] | |
55 foo = baz | |
56 in the settings file. Options that do not exist on assignment | |
57 are created dynamcially. | |
58 | |
59 Values are automatically converted to the appropriate python type. | |
60 Options that begin and end with brackets([, ]) are converted to lists, | |
61 and options that are double-quoted (") are converted to strings. | |
62 Section also recognizes booleans regardless of case, in addition to the | |
63 literals 'yes' and 'no' of any case. Except in the case of | |
64 double-quoted strings, extra white-space is trimmed, so you need not | |
65 worry. For example: | |
66 foo = bar | |
67 is equivalent to : | |
68 foo = baz | |
69 """ | |
70 def __init__(self, name): | |
71 """ Initialize a new section. | |
72 | |
73 @param name: name of the section. In the INI file, sections are surrounded | |
74 by brackets ([name]) | |
75 @type name: string | |
76 """ | |
77 self.name = name | |
78 | |
79 def __setattr__(self, option, value): | |
80 """ Assign a value to an option, converting types when appropriate. | |
81 | |
82 @param option: name of the option to assign a value to. | |
83 @type option: string @param value: value to be assigned to the option. | |
84 @type value: int, float, string, boolean, or list | |
85 """ | |
86 value = str(value) | |
87 if value.startswith('[') and value.endswith(']'): | |
88 value = [item.strip() for item in value[1:-1].split(',')] | |
89 elif value.lower() == 'true' or value.lower() == 'yes': | |
90 value = True | |
91 elif value.lower() == 'false' or value.lower() == 'no': | |
92 value = False | |
93 elif value.isdigit(): | |
94 value = int(value) | |
95 else: | |
96 try: | |
97 value = float(value) | |
98 except ValueError: | |
99 # leave as string | |
100 pass | |
101 | |
102 self.__dict__[option] = value | |
103 | |
104 def __getattribute__(self, option): | |
105 """ Returns the option's value""" | |
106 # Remove leading and trailing quotes from strings that have them | |
107 return_value = object.__getattribute__(self, option) | |
108 try: | |
109 for key, value in return_value.iteritems(): | |
110 if (hasattr(value, 'split') and | |
111 value.startswith("\"") and value.endswith("\"")): | |
112 return_value[key] = value[1:-1] | |
113 except AttributeError: | |
114 pass | |
115 | |
116 return return_value | |
117 | |
118 @property | |
119 def options(self): | |
120 """ Returns a dictionary of existing options """ | |
121 options = self.__dict__ | |
122 # get rid of properties that aren't actually options | |
123 if options.has_key('name'): | |
124 options.pop('name') | |
125 | |
126 return options | |
127 | |
128 class Settings(object): | |
129 """ An object that represents a settings file, its sectons, | |
130 and the options defined within those sections. | |
131 """ | |
132 def __init__(self, settings_path='', system_path='', user_path='', suffix='.cfg'): | |
133 """ initializes a new settings object. If no paths are given, they are | |
134 guessed based on whatever platform the script was run on. | |
135 | |
136 Examples: | |
137 paths = ['/etc/parpg', '/home/user_name/.config/parpg'] | |
138 settings = Settings(*paths) | |
139 | |
140 paths = {'system': '/etc/parpg', | |
141 'user': '/home/user_name/.config/parpg'} | |
142 settings = Settings(**paths) | |
143 | |
144 settings = Settings('.') | |
145 | |
146 settigns = Settings() | |
147 | |
148 @param system_path: Path to the system settings file. | |
149 @type system_path: string (must be a valid path) | |
150 | |
151 @param user_path: Path to the user settings file. Options that | |
152 are missing from this file are propogated | |
153 from the system settings file and saved on | |
154 request | |
155 @type user_path: string (must be a valid path) | |
156 | |
157 @param suffix: Suffix of the settings file that will be generated. | |
158 @type suffix: string | |
159 """ | |
160 if not suffix.startswith('.'): | |
161 suffix = '.' + suffix | |
162 | |
163 self.suffix = suffix | |
164 self.settings_file = '' | |
165 | |
166 | |
167 self.paths = {} | |
168 if not system_path and not user_path and not settings_path: | |
169 # use platform-specific values as paths | |
170 (self.paths['system'], self.paths['user'], | |
171 self.paths['settings']) = self.platform_paths() | |
172 else: | |
173 # convert supplied paths to absolute paths | |
174 abs_paths = [os.path.expanduser(path) | |
175 for path in [system_path, user_path, settings_path]] | |
176 (self.paths['system'], self.paths['user'], | |
177 self.paths['settings']) = abs_paths | |
178 | |
179 self.read() | |
180 | |
181 | |
182 def __getattr__(self, name): | |
183 """ Returns a Section object to be used for assignment, creating one | |
184 if it doesn't exist. | |
185 | |
186 @param name: name of section to be retrieved | |
187 @type name: string | |
188 """ | |
189 if name in ['get', 'set']: | |
190 raise AttributeError("{0} is deprecated. Please consult Settings' " | |
191 "documentation for information on how to " | |
192 "create/modify sections and their respective " | |
193 "options".format(name)) | |
194 else: | |
195 if not self.__dict__.has_key(name): | |
196 setattr(self, name, Section(name)) | |
197 | |
198 return getattr(self, name) | |
199 | |
200 def platform_paths(self, system=None): | |
201 if system is None: | |
202 system = platform.system().lower() | |
203 | |
204 if system == 'linux': | |
205 return (os.path.join(os.sep, 'usr', 'share', 'parpg'), | |
206 os.path.join(os.environ['XDG_CONFIG_HOME'], 'parpg'), | |
207 os.path.join(os.sep, 'etc', 'parpg')) | |
208 elif system == 'windows': | |
209 return (os.path.join(os.environ['PROGRAMFILES'], 'PARPG'), | |
210 os.path.join(os.environ['USERDATA'], 'PARPG'), | |
211 os.path.join(os.environ['PROGRAMFILES'], 'PARPG')) | |
212 else: | |
213 # TODO: determine values for Mac | |
214 return None | |
215 | |
216 def read(self, filenames=None): | |
217 """ Reads a settings file and populates the settings object | |
218 with its sections and options. Calling this method without | |
219 any arguments simply re-reads the previously defined filename | |
220 and paths | |
221 | |
222 @param filenames: name of files to be parsed. | |
223 @type path: string or list | |
224 """ | |
225 | |
226 if filenames is None: | |
227 filenames = [os.path.join(self.paths['settings'], | |
228 'system{0}'.format(self.suffix)), | |
229 os.path.join(self.paths['user'], | |
230 'user{0}'.format(self.suffix))] | |
231 elif hasattr(filenames, 'split'): | |
232 filenames = [filenames] | |
233 | |
234 for filename in filenames: | |
235 section = None | |
236 | |
237 try: | |
238 self.settings_file = open(filename, 'r').readlines() | |
239 except IOError as (errno, strerror): | |
240 if errno == 2: | |
241 if os.path.basename(filename).startswith('system'): | |
242 print ('{0} could not be found. Please supply a ' | |
243 'different path or generate a system settings ' | |
244 'file with:\n' | |
245 'python2 -m parpg.settings').format(filename) | |
246 sys.exit(1) | |
247 else: | |
248 print 'Error No. {0}: {1} {2}'.format(errno, filename, strerror) | |
249 sys.exit(1) | |
250 | |
251 for line in self.settings_file: | |
252 if line.startswith('#') or line.strip() == '': | |
253 continue | |
254 elif line.startswith('[') and line.endswith(']\n'): | |
255 getattr(self, line[1:-2]) | |
256 section = line[1:-2] | |
257 else: | |
258 option, value = [item.strip() | |
259 for item in line.split('=', 1)] | |
260 setattr(getattr(self, section), option, value) | |
261 | |
262 def write(self, filename=None): | |
263 """ Writes a settings file based on the settings object's | |
264 sections and options | |
265 | |
266 @param filename: Name of file to save to. By default, this is | |
267 the user settings file. | |
268 @type path: string | |
269 """ | |
270 if filename is None: | |
271 filename = os.path.join(self.paths['user'], | |
272 'user{0}'.format(self.suffix)) | |
273 | |
274 for section in self.sections: | |
275 if '[{0}]\n'.format(section) not in self.settings_file: | |
276 self.settings_file.append('\n[{0}]\n'.format(section)) | |
277 for option, value in getattr(self, section).options.iteritems(): | |
278 template = '{0} = {1}\n'.format(option, value) | |
279 self.settings_file.append(template) | |
280 else: | |
281 start_of_section = (self.settings_file | |
282 .index('[{0}]\n'.format(section)) + 1) | |
283 | |
284 for option, value in getattr(self, | |
285 section).options.iteritems(): | |
286 if hasattr(value, 'sort'): | |
287 value = '[{0}]'.format(', '.join(value)) | |
288 | |
289 new_option = False | |
290 template = '{0} = {1}\n'.format(option, value) | |
291 for index, line in enumerate(self.settings_file[:]): | |
292 if option in line: | |
293 new_option = False | |
294 if str(value) not in line: | |
295 self.settings_file[index] = template | |
296 | |
297 break | |
298 else: | |
299 new_option = True | |
300 if new_option: | |
301 while self.settings_file[start_of_section].startswith('#'): | |
302 start_of_section += 1 | |
303 | |
304 self.settings_file.insert(start_of_section, template) | |
305 | |
306 with open(filename, 'w') as out_stream: | |
307 for line in self.settings_file: | |
308 out_stream.write(line) | |
309 | |
310 @property | |
311 def sections(self): | |
312 """ Returns a list of existing sections""" | |
313 sections = self.__dict__.keys() | |
314 sections.pop(sections.index('settings_file')) | |
315 sections.pop(sections.index('paths')) | |
316 sections.pop(sections.index('suffix')) | |
317 | |
318 return sections | |
319 | |
320 @property | |
321 def system_path(self): | |
322 return self.paths['system'] | |
323 | |
324 @property | |
325 def user_path(self): | |
326 return self.paths['user'] | |
327 | |
328 @property | |
329 def settings_path(self): | |
330 return self.paths['settings'] | |
331 | |
332 DEFAULT_SETTINGS = """\ | |
333 [fife] | |
334 #------------------------------------------------------------------------------ | |
335 # Options marked with ? are untested/unknown | |
336 | |
337 # Game window's title (string) DO NOT EDIT! | |
338 WindowTitle = PARPG Techdemo 2 | |
339 | |
340 # Icon to use for the game window's border (filename) DO NOT EDIT! | |
341 WindowIcon = window_icon.png | |
342 | |
343 # Video driver to use. (?) | |
344 VideoDriver = "" | |
345 | |
346 # Backend to use for graphics (OpenGL|SDL) | |
347 RenderBackend = OpenGL | |
348 | |
349 # Run the game in fullscreen mode or not. (True|False) | |
350 FullScreen = False | |
351 | |
352 # Screen Resolution's width. Not used if FullScreen is set to False (800|1024|etc) | |
353 ScreenWidth = 1024 | |
354 | |
355 # Screen Resolution's height. Not used if FullScreen is set to False (600|768|etc) | |
356 ScreenHeight = 768 | |
357 | |
358 # Screen DPI? (?) | |
359 BitsPerPixel = 0 | |
360 | |
361 # ? (?) | |
362 SDLRemoveFakeAlpha = 1 | |
363 | |
364 # Subdirectory to load icons from (path) | |
365 IconsPath = icons | |
366 | |
367 # ? ([R, G, B]) | |
368 ColorKey = [250, 0, 250] | |
369 | |
370 # ? (True|False) | |
371 ColorKeyEnabled = False | |
372 | |
373 # Turn on sound effects and music (True|False) | |
374 EnableSound = True | |
375 | |
376 # Initial volume of sound effects and music (0.0-100.0?) | |
377 InitialVolume = 5.0 | |
378 | |
379 # Characters to use to render fonts. DO NOT EDIT! | |
380 FontGlyphs = " abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.,!?-+/():;%&`'*#=[]\"" | |
381 | |
382 # Subdirectory to load fronts from (path) | |
383 FontsPath = fonts | |
384 | |
385 # Font to load when game starts | |
386 Font = oldtypewriter.ttf | |
387 | |
388 # Size of in-game fonts | |
389 DefaultFontSize = 12 | |
390 | |
391 # ? (?) | |
392 LogModules = [controller] | |
393 | |
394 # ? (?) | |
395 PychanDebug = False | |
396 | |
397 # use Psyco Acceperation (True|False) | |
398 UsePsyco = False | |
399 | |
400 # ? (?) | |
401 ProfilingOn = False | |
402 | |
403 # Lighting Model to use (0-2) | |
404 Lighting = 0 | |
405 | |
406 [parpg] | |
407 #------------------------------------------------------------------------------ | |
408 | |
409 # System subdirectory to load maps from (path) | |
410 MapsPath = maps | |
411 | |
412 # YAML file that contains the available maps (filename) | |
413 MapsFile = maps.yaml | |
414 | |
415 # Map to load when game starts (filename) | |
416 Map = Mall | |
417 | |
418 # ? (filename) | |
419 AllAgentsFile = all_agents.yaml | |
420 | |
421 # System subdirectory to load objects from (path) | |
422 ObjectsPath = objects | |
423 | |
424 # YAML file that contains the database of availabel objects (filename) | |
425 ObjectDatabaseFile = object_database.yaml | |
426 | |
427 # System subdirectory to load dialogues from (path) | |
428 DialoguesPath = dialogue | |
429 | |
430 # System subdirectory to load quests from (path) | |
431 QuestsPath = quests | |
432 | |
433 # User subdirectory to save screenshots to | |
434 ScreenshotsPath = screenshots | |
435 | |
436 # User subdirectory to save games to | |
437 SavesPath = saves | |
438 | |
439 # System subdirectory where gui files are loaded from (path) | |
440 GuiPath = gui | |
441 | |
442 # System subdirectory where cursors are loaded from (path) | |
443 CursorPath = cursors | |
444 | |
445 # File to use for default cursor (filename) | |
446 CursorDefault = cursor_plain.png | |
447 | |
448 # File to use for up cursor (filename) | |
449 CursorUp = cursor_up.png | |
450 | |
451 # File to use for right cursor (filename) | |
452 CursorRight = cursor_right.png | |
453 | |
454 # File to use for down cursor (filename) | |
455 CursorDown = cursor_down.png | |
456 | |
457 # File to use for left cursor (filename) | |
458 CursorLeft = cursor_left.png | |
459 | |
460 # Player walk speed (digit) | |
461 PCSpeed = 3\ | |
462 """ | |
463 | |
464 if __name__ == '__main__': | |
465 from optparse import OptionParser | |
466 | |
467 usage = "usage: %prog [options] system[, system, ...]" | |
468 parser = OptionParser(usage=usage) | |
469 | |
470 parser.add_option('-f', '--filename', default='system.cfg', | |
471 help='Filename of output configuration file') | |
472 | |
473 opts, args = parser.parse_args() | |
474 | |
475 with open(opts.filename, 'w') as f: | |
476 for line in DEFAULT_SETTINGS: | |
477 f.write(line) |