Source code for todopy.options

# vim: set fileencoding=utf-8 :
# Todo.txt manager in Python
# Copyright (C) 2011 Ilkka Laukkanen <ilkka.s.laukkanen@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

from argparse import ArgumentParser
import os.path
import sys
from configparser import ConfigParser
import re

[docs]class Options(object): """Combined command line args and config file parser. There are three types of args: flags, options and positional arguments. Flags have boolean values, options and positionals unicode values. >>> o = Options('description') >>> o.add_flag('my_flag', help='Help for my flag') >>> o.add_flag('other_flag', help='Help for my other flag') >>> o.add_flag('yet_another', help='Yet another flag') >>> o.add_option('thingy', group='other', help='Set thingy') >>> o.add_flag('flaggy', group='other') >>> o.add_argument('widget') Arguments also have optional counts: '*' for 0..n, '+' for 1..n or any integer. Specifying a count makes the argument's value always be a list. Not specifying the count makes the value be a plain value. You can pass the 'dest' keyword-only argument to add_argument for nicer, pluralized access to list-valued arguments. >>> o.add_argument('id', dest='ids', group='other', type=int, count='*') >>> o.parse(argv=['--my_flag', '-y', '--thingy', 'babar 1234', 'frobozz', '1', '2', '3']) >>> o.main.my_flag True >>> o.main.other_flag False >>> o.main.yet_another True >>> o.other.thingy u'babar 1234' >>> o.other.flaggy False >>> o.main.widget u'frobozz' >>> o.other.ids [1, 2, 3] """ def __init__(self, description=None, **kwargs): self.parser = ArgumentParser(description=description) self.args = None self.config = ConfigParser() self.defaults = {} self.used_optdefs = [] self.valuators = {} self.sub = None self.subparsers = {} self.command = None self.has_been_parsed = False @staticmethod def _booleanify(val): """Convert non-boolean option values to boolean. >>> Options._booleanify(u'yes') True >>> Options._booleanify('no') False >>> Options._booleanify('TrUE') True >>> Options._booleanify(False) False """ if not (type(val) == str or type(val) == unicode): return bool(val) defs = { u'yes': True, u'true': True, u'no': False, u'false': False } val = unicode(val.lower()) if val in defs: return defs[val] return None def __getattr__(self, attr): class Optgroup: def __init__(self, options, group): self.options = options self.group = group def __getattr__(self, attr): if getattr(self.options.args, attr) != None: return self.options.valuators[self.group][attr](getattr(self.options.args, attr)) elif self.group in self.options.config and attr in self.options.config[self.group]: return self.options.valuators[self.group][attr](self.options.config[self.group][attr]) else: return self.options.defaults[self.group][attr] return Optgroup(self, attr) def _make_short_opt_for(self, option): """Make short one-character option for given option name. Remember what optchars have been used and don't return them again. Also try very hard not to return any reserved optchars (like '-h' for help and '-C' for config file). >>> o = Options() >>> o._make_short_opt_for('foobar') '-f' >>> o._make_short_opt_for('fuzzball') '-u' >>> o._make_short_opt_for('fun') '-n' >>> o._make_short_opt_for('fun') '-F' """ for c in re.sub('r[hC]', '', option + option.upper() + "abcdefghijklmnopqrstuvxyzABCDEFGHIJKLMNOPQRSTUVXYZ"): if c == 'h': continue # try not to clobber help command opt = "-{}".format(c) if not opt in self.used_optdefs: self.used_optdefs.append(opt) return opt raise RuntimeError("Couldn't determine optchar for {}".format(option))
[docs] def add_flag(self, flag, **kwargs): """Add a boolean flag. Command line options will be derived from flag name. Config file setting will have the same name as the flag, and the value will be accessible via an attribute with the same name also. If group keyword-only argument is not specified, the flag will be in the 'main' group. If default keyword-only argument is not specified, the default is True. Help text is specified with the help keyword- only argument. """ default = kwargs['default'] if 'default' in kwargs else False group = kwargs['group'] if 'group' in kwargs else 'main' help = kwargs['help'] if 'help' in kwargs else None parser = self.subparsers[group] if group in self.subparsers else self.parser parser.add_argument(self._make_short_opt_for(flag), "--{}".format(flag), dest=flag, action='store_const', const=not default, help=help) if group not in self.defaults: self.defaults[group] = {} if group not in self.valuators: self.valuators[group] = {} self.defaults[group][flag] = default self.valuators[group][flag] = self._booleanify
[docs] def add_option(self, option, **kwargs): """Add an option taking a unicode argument. Command line options will be derived from option name. Config file setting will have the same name as the option, and the value will be accessible via an attribute with the same name also. If group keyword-only argument is not specified, the option will be in the 'main' group. If default keyword-only argument is not specified, the default is None. Help text is specified with the help keyword- only argument. """ default = kwargs['default'] if 'default' in kwargs else None group = kwargs['group'] if 'group' in kwargs else 'main' help = kwargs['help'] if 'help' in kwargs else None parser = self.subparsers[group] if group in self.subparsers else self.parser parser.add_argument(self._make_short_opt_for(option), "--{}".format(option), dest=option, metavar=option.upper(), type=unicode, help=help) if group not in self.defaults: self.defaults[group] = {} if group not in self.valuators: self.valuators[group] = {} self.defaults[group][option] = default self.valuators[group][option] = unicode
[docs] def parse(self, **kwargs): """Parse command line and config file. By default parser sys.argv, but command line args can be passed as an array too. To parse a config file too, pass 'config_file' kwarg. Passing that argument will also enable the config file command line option, with the default set to whatever the value of the kwarg is. >>> import tempfile; import os >>> (handle, filename) = tempfile.mkstemp(); os.close(handle) >>> f = open(filename, 'w+') >>> f.write("[main]\\n") >>> f.write("my_flag = yes\\n") >>> f.write("[mygroup]\\n") >>> f.write("a_setting = blah blah raspberry 3.14159") >>> f.close() >>> o = Options() >>> o.add_flag('my_flag', default=False) >>> o.add_flag('other_flag', default=False) >>> o.add_option('a_setting', group='mygroup') >>> o.parse(argv=['-o'], config_file=filename) >>> o.main.my_flag True >>> o.main.other_flag True >>> o.mygroup.a_setting u'blah blah raspberry 3.14159' parse() can only be called once for a given Options instance. >>> o.parse(argv=['--my_flag']) Traceback (most recent call last): ... RuntimeError: parse can only be called once for any given Options instance. Config file overrides defaults, and command line overrides config file. >>> o = Options() >>> o.add_flag('my_flag', default=True) >>> o.add_option('a_setting', group='mygroup', default='asdf') >>> o.parse(argv=['--my_flag'], config_file=filename) >>> o.main.my_flag False >>> o.mygroup.a_setting u'blah blah raspberry 3.14159' """ if self.has_been_parsed: raise RuntimeError("parse can only be called once for any given Options instance.") # Add config file arg here to not clobber any user stuff if 'config_file' in kwargs: self.parser.add_argument('-C', '--config_file', dest='config_file', type=unicode, default=kwargs['config_file']) self.args = self.parser.parse_args(kwargs['argv']) if 'argv' in kwargs \ else self.parser.parse_args() self.command = unicode(self.args.command) if hasattr(self.args, 'command') else None if 'config_file' in self.args: self.config.read(self.args.config_file) self.has_been_parsed = True
[docs] def add_subcommand(self, command, **kwargs): """Add a subcommand. Subcommands create a new argument group. To add a subcommand-specific command-line argument and option, pass the subcommand's name as group. >>> o = Options() >>> o.add_subcommand('frob', help='frob the widget') >>> o.add_flag('foo') >>> o.add_flag('bar') >>> o.add_flag('baz', group='frob') >>> o.parse(argv=['-f', 'frob', '--baz']) After parsing, the subcommand used is available as the "command" attribute. >>> o.command u'frob' >>> o.frob.baz True """ help = kwargs['help'] if 'help' in kwargs else None if not self.sub: self.sub = self.parser.add_subparsers(dest='command', title='Subcommands') self.subparsers[command] = self.sub.add_parser(command, help=help)
[docs] def add_argument(self, argument, **kwargs): """Add a positional argument. """ group = kwargs['group'] if 'group' in kwargs else 'main' help = kwargs['help'] if 'help' in kwargs else None count = kwargs['count'] if 'count' in kwargs else None dest = kwargs['dest'] if 'dest' in kwargs else argument type = kwargs['type'] if 'type' in kwargs else unicode parser = self.subparsers[group] if group in self.subparsers else self.parser parser.add_argument(dest, metavar=argument.upper(), type=type, nargs=count, help=help) if group not in self.valuators: self.valuators[group] = {} self.valuators[group][dest] = lambda x: x