# HG changeset patch # User Emmanuel Gil Peyrot # Date 1404249460 -7200 # Node ID b2269b9c6119b59369387ed370eeb852c32ecee5 # Parent 04ae31809dc71f7e1b4dc04e30a7be97cc05bbb7 Add a configuration parser, and pass those options to argparse as defaults. Also include an xdg helper. diff --git a/pytouhou/options.py b/pytouhou/options.py --- 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() diff --git a/pytouhou/utils/xdg.py b/pytouhou/utils/xdg.py 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 diff --git a/scripts/pytouhou b/scripts/pytouhou --- 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