changeset 567:b2269b9c6119

Add a configuration parser, and pass those options to argparse as defaults. Also include an xdg helper.
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Tue, 01 Jul 2014 23:17:40 +0200
parents 04ae31809dc7
children e7a4731a278b
files pytouhou/options.py pytouhou/utils/xdg.py scripts/pytouhou
diffstat 3 files changed, 176 insertions(+), 24 deletions(-) [+]
line wrap: on
line diff
--- a/pytouhou/options.py
+++ b/pytouhou/options.py
@@ -12,25 +12,108 @@
 ## GNU General Public License for more details.
 ##
 
-from argparse import ArgumentParser
+import os
+from ConfigParser import RawConfigParser, NoOptionError
+
+from pytouhou.utils.xdg import load_config_paths
+
+
+class Options(object):
+    def __init__(self, name, defaults):
+        load_paths = list(reversed([os.path.join(directory, '%s.cfg' % name)
+                                    for directory
+                                    in load_config_paths(name)]))
+
+        self.config = RawConfigParser(defaults)
+        self.paths = self.config.read(load_paths)
+        self.section = name if self.config.has_section(name) else 'DEFAULT'
+
+    def get(self, option):
+        try:
+            return self.config.get(self.section, option)
+        except NoOptionError:
+            return None
+
+
+def patch_argument_parser():
+    from argparse import ArgumentParser, _ActionsContainer
+
+    original_method = _ActionsContainer.add_argument
+
+    def add_argument(self, *args, **kwargs):
+        if 'default' not in kwargs:
+            dest = kwargs.get('dest')
+            if dest is None:
+                for dest in args:
+                    dest = dest.lstrip('-')
+                    value = self.default.get(dest)
+                    if value is not None:
+                        break
+            else:
+                dest = dest.replace('_', '-')
+                value = self.default.get(dest)
+            if value is not None:
+                argument_type = kwargs.get('type')
+                if argument_type is not None:
+                    value = argument_type(value)
+                action = kwargs.get('action')
+                if action == 'store_true':
+                    value = value.lower() == 'true'
+                elif action == 'store_false':
+                    value = value.lower() != 'true'
+                if kwargs.get('nargs') == '*' and isinstance(value, str):
+                    value = value.split()
+                kwargs['default'] = value
+            elif dest == 'double-buffer':
+                kwargs['default'] = None
+        return original_method(self, *args, **kwargs)
+    _ActionsContainer.add_argument = add_argument
+
+    class Parser(ArgumentParser):
+        def __init__(self, *args, **kwargs):
+            self.default = kwargs.pop('default')
+            ArgumentParser.__init__(self, *args, **kwargs)
+
+        def add_argument_group(self, *args, **kwargs):
+            group = ArgumentParser.add_argument_group(self, *args, **kwargs)
+            group.default = self.default
+            group.add_argument_group = self.add_argument_group
+            group.add_mutually_exclusive_group = self.add_mutually_exclusive_group
+            return group
+
+        def add_mutually_exclusive_group(self, *args, **kwargs):
+            group = ArgumentParser.add_mutually_exclusive_group(self, *args, **kwargs)
+            group.default = self.default
+            group.add_argument_group = self.add_argument_group
+            group.add_mutually_exclusive_group = self.add_mutually_exclusive_group
+            return group
+
+    return Parser
+
+
+ArgumentParser = patch_argument_parser()
+
+
+def parse_config(section, defaults):
+    return Options(section, defaults)
 
 
 def parse_arguments(defaults):
-    parser = ArgumentParser(description='Libre reimplementation of the Touhou 6 engine.')
+    parser = ArgumentParser(description='Libre reimplementation of the Touhou 6 engine.', default=defaults)
 
-    parser.add_argument('data', metavar='DAT', default=defaults['data'], nargs='*', help='Game’s data files')
-    parser.add_argument('-p', '--path', metavar='DIRECTORY', default='.', help='Game directory path.')
+    parser.add_argument('data', metavar='DAT', nargs='*', help='Game’s data files')
+    parser.add_argument('-p', '--path', metavar='DIRECTORY', help='Game directory path.')
     parser.add_argument('--debug', action='store_true', help='Set unlimited continues, and perhaps other debug features.')
     parser.add_argument('--verbosity', metavar='VERBOSITY', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], help='Select the wanted logging level.')
 
     game_group = parser.add_argument_group('Game options')
-    game_group.add_argument('-s', '--stage', metavar='STAGE', type=int, default=None, help='Stage, 1 to 7 (Extra), nothing means story mode.')
-    game_group.add_argument('-r', '--rank', metavar='RANK', type=int, default=0, help='Rank, from 0 (Easy, default) to 3 (Lunatic).')
-    game_group.add_argument('-c', '--character', metavar='CHARACTER', type=int, default=0, help='Select the character to use, from 0 (ReimuA, default) to 3 (MarisaB).')
+    game_group.add_argument('-s', '--stage', metavar='STAGE', type=int, help='Stage, 1 to 7 (Extra), nothing means story mode.')
+    game_group.add_argument('-r', '--rank', metavar='RANK', type=int, help='Rank, from 0 (Easy, default) to 3 (Lunatic).')
+    game_group.add_argument('-c', '--character', metavar='CHARACTER', type=int, help='Select the character to use, from 0 (ReimuA, default) to 3 (MarisaB).')
     game_group.add_argument('-b', '--boss-rush', action='store_true', help='Fight only bosses.')
-    game_group.add_argument('--game', metavar='GAME', choices=['EoSD'], default='EoSD', help='Select the game engine to use.')
-    game_group.add_argument('--interface', metavar='INTERFACE', choices=['EoSD', 'Sample'], default='EoSD', help='Select the interface to use.')
-    game_group.add_argument('--hints', metavar='HINTS', default=None, help='Hints file, to display text while playing.')
+    game_group.add_argument('--game', metavar='GAME', choices=['EoSD'], help='Select the game engine to use.')
+    game_group.add_argument('--interface', metavar='INTERFACE', choices=['EoSD', 'Sample'], help='Select the interface to use.')
+    game_group.add_argument('--hints', metavar='HINTS', help='Hints file, to display text while playing.')
 
     replay_group = parser.add_argument_group('Replay options')
     replay_group.add_argument('--replay', metavar='REPLAY', help='Select a file to replay.')
@@ -38,23 +121,23 @@ def parse_arguments(defaults):
     replay_group.add_argument('--skip-replay', action='store_true', help='Skip the replay and start to play when it’s finished.')
 
     netplay_group = parser.add_argument_group('Netplay options')
-    netplay_group.add_argument('--port', metavar='PORT', type=int, default=0, help='Local port to use.')
-    netplay_group.add_argument('--remote', metavar='REMOTE', default=None, help='Remote address.')
+    netplay_group.add_argument('--port', metavar='PORT', type=int, help='Local port to use.')
+    netplay_group.add_argument('--remote', metavar='REMOTE', help='Remote address.')
     netplay_group.add_argument('--friendly-fire', action='store_true', help='Allow friendly-fire during netplay.')
 
     graphics_group = parser.add_argument_group('Graphics options')
-    graphics_group.add_argument('--backend', metavar='BACKEND', choices=['opengl', 'sdl'], default=['opengl', 'sdl'], nargs='*', help='Which backend to use (opengl or sdl).')
-    graphics_group.add_argument('--fps-limit', metavar='FPS', default=-1, type=int, help='Set fps limit. A value of 0 disables fps limiting, while a negative value limits to 60 fps if and only if vsync doesn’t work.')
+    graphics_group.add_argument('--backend', metavar='BACKEND', choices=['opengl', 'sdl'], nargs='*', help='Which backend to use (opengl or sdl).')
+    graphics_group.add_argument('--fps-limit', metavar='FPS', type=int, help='Set fps limit. A value of 0 disables fps limiting, while a negative value limits to 60 fps if and only if vsync doesn’t work.')
     graphics_group.add_argument('--no-background', action='store_false', help='Disable background display (huge performance boost on slow systems).')
     graphics_group.add_argument('--no-particles', action='store_false', help='Disable particles handling (huge performance boost on slow systems).')
     graphics_group.add_argument('--no-sound', action='store_false', help='Disable music and sound effects.')
 
     opengl_group = parser.add_argument_group('OpenGL backend options')
-    opengl_group.add_argument('--gl-flavor', choices=['core', 'es', 'compatibility', 'legacy'], default='compatibility', help='OpenGL profile to use.')
-    opengl_group.add_argument('--gl-version', default=2.1, type=float, help='OpenGL version to use.')
+    opengl_group.add_argument('--gl-flavor', choices=['core', 'es', 'compatibility', 'legacy'], help='OpenGL profile to use.')
+    opengl_group.add_argument('--gl-version', type=float, help='OpenGL version to use.')
 
     double_buffer = opengl_group.add_mutually_exclusive_group()
-    double_buffer.add_argument('--double-buffer', dest='double_buffer', action='store_true', default=None, help='Enable double buffering.')
-    double_buffer.add_argument('--single-buffer', dest='double_buffer', action='store_false', default=None, help='Disable double buffering.')
+    double_buffer.add_argument('--double-buffer', dest='double_buffer', action='store_true', help='Enable double buffering.')
+    double_buffer.add_argument('--single-buffer', dest='double_buffer', action='store_false', help='Disable double buffering.')
 
     return parser.parse_args()
new file mode 100644
--- /dev/null
+++ b/pytouhou/utils/xdg.py
@@ -0,0 +1,55 @@
+# -*- encoding: utf-8
+"""
+This module is based on a rox module (LGPL):
+
+http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/basedir.py?rev=1.9&view=log
+
+The freedesktop.org Base Directory specification provides a way for
+applications to locate shared data and configuration:
+
+    http://standards.freedesktop.org/basedir-spec/
+
+(based on version 0.6)
+
+This module can be used to load and save from and to these directories.
+
+Typical usage:
+
+    from rox import basedir
+    
+    for dir in basedir.load_config_paths('mydomain.org', 'MyProg', 'Options'):
+        print "Load settings from", dir
+
+    dir = basedir.save_config_path('mydomain.org', 'MyProg')
+    print >>file(os.path.join(dir, 'Options'), 'w'), "foo=2"
+
+Note: see the rox.Options module for a higher-level API for managing options.
+"""
+
+import os
+
+_home = os.path.expanduser('~')
+xdg_config_home = os.environ.get('XDG_CONFIG_HOME') or \
+    os.path.join(_home, '.config')
+
+xdg_config_dirs = [xdg_config_home] + \
+    (os.environ.get('XDG_CONFIG_DIRS') or '/etc/xdg').split(':')
+
+xdg_config_dirs = [x for x in xdg_config_dirs if x]
+
+
+def save_config_path(*resource):
+    resource = os.path.join(*resource)
+    assert not resource.startswith('/')
+    path = os.path.join(xdg_config_home, resource)
+    if not os.path.isdir(path):
+        os.makedirs(path, 0o700)
+    return path
+
+
+def load_config_paths(*resource):
+    resource = os.path.join(*resource)
+    for config_dir in xdg_config_dirs:
+        path = os.path.join(config_dir, resource)
+        if os.path.exists(path):
+            yield path
--- a/scripts/pytouhou
+++ b/scripts/pytouhou
@@ -20,12 +20,24 @@ default_data = (pathsep.join(('CM.DAT', 
                 pathsep.join(('MD.DAT', 'th6*MD.DAT', '*MD.DAT', '*md.dat')),
                 pathsep.join(('102h.exe', '102*.exe', '東方紅魔郷.exe', '*.exe')))
 
-defaults = {'data': default_data}
+defaults = {'data': default_data,
+            'path': '.',
+            'rank': 0,
+            'character': 0,
+            'game': 'EoSD',
+            'interface': 'EoSD',
+            'port': 0,
+            'backend': ['opengl', 'sdl'],
+            'gl-flavor': 'compatibility',
+            'gl-version': 2.1,
+            'double-buffer': None,
+            'fps-limit': -1}
 
-from pytouhou.options import parse_arguments
-args = parse_arguments(defaults)
+from pytouhou.options import parse_config, parse_arguments
+options = parse_config('pytouhou', defaults)
+args = parse_arguments(options)
 
-verbosity = args.verbosity or 'WARNING'
+verbosity = args.verbosity or options.get('verbosity') or 'WARNING'
 
 import logging
 logging.basicConfig(level=getattr(logging, verbosity),
@@ -34,6 +46,8 @@ logging.basicConfig(level=getattr(loggin
 logger = logging
 logger.root.name = 'pytouhou'
 
+logger.info('Configuration loaded from: %s', ', '.join(options.paths))
+
 if args.game == 'EoSD':
     from pytouhou.games.eosd import EoSDCommon as Common, EoSDGame as Game
 
@@ -69,13 +83,13 @@ for backend_name in args.backend:
     try:
         backend = import_module('pytouhou.ui.%s.backend' % backend_name)
     except ImportError as e:
-        logger.error('Failed to import backend %s: %s', backend_name, e)
+        logger.exception('Failed to import backend %s:', backend_name)
         continue
 
     try:
         backend.init(options)
     except Exception as e:
-        logger.error('Backend %s failed to initialize: %s', backend_name, e)
+        logger.exception('Backend %s failed to initialize:', backend_name)
         continue
 
     GameRenderer = backend.GameRenderer