''' parsing options with a nicer wrapper around getopt. Still throws getopt.GetoptError at runtime. Let's try to combine this with basic html form parsing, so that we can declare the options just once. Can we make this a bit more elegant by using class attributes and more subclassing than instantiation? The problem then is, of course, that client code will have to do both. ''' import getopt, textwrap, re class OptionError(Exception): pass class Option(object): collapseWs = re.compile('\s+') form_tag_template = "" def __init__(self, long_name, short_name, form_text=None, key=None, default=None, valid_range=None, help_text="""Our engineers deemed it self-explanatory"""): self.long_name = long_name self.short_name = short_name self.key = key or long_name self.form_text = form_text or long_name self.valid_range = valid_range # must precede assignment of self.default if default is None: default = self._default() self.default = self.value = default self.help_text = help_text def _default(self): return None def validate_range(self, value): ''' can be overridden if more general tests are needed ''' return self.valid_range is None or value in self.valid_range def validate(self, value): success, converted = self._validate(value) success = success and self.validate_range(converted) if success: self.value = converted return True return False def validate_form_value(self, value): ''' validation of option value received through a web form. May need to be different from CLI, but by default it's not. ''' return self.validate(value) def _validate(self, value): ''' no-op default ''' return True, value def short_getopt(self): ''' short option template for getopt ''' return self.short_name + ':' def long_getopt(self): ''' long option template for getopt ''' return self.long_name + '=' def format_help(self, indent=30, linewidth=80): ''' format option and help text for console display maybe we can generalize this for html somehow ''' help_text = '%s (Default: %s)' % (self.help_text, self.default) help_text = self.collapseWs.sub(' ', help_text.strip()) hwrap = textwrap.wrap(help_text, width = linewidth, initial_indent=' ' * indent, subsequent_indent= ' ' * indent) opts = '-%s, --%s' % (self.short_name, self.long_name) hwrap[0] = opts.ljust(indent) + hwrap[0].lstrip() return hwrap def format_tag_value(self, value): ''' format the default value for insertion into form tag ''' if value is None: return '' return str(value) def format_tag(self, value=None): ''' render a html form tag ''' value = value or self.default values = dict(key=self.key, value=self.format_tag_value(value) ) tag = self.form_tag_template % values return self.key, tag, self.form_text, self.help_text class BoolOption(Option): form_tag_template = r'''''' def _default(self): return False def validate(self, value=None): ''' value should be empty; we accept and discard it. we simply switch the default value. ''' self.value = not self.default return True def validate_form_value(self, value): ''' if a value arrives through a web form, the box has been ticked, so we set to True regardless of default. The passed value itself is unimportant. ''' self.value = True return True def short_getopt(self): return self.short_name def long_getopt(self): return self.long_name def format_tag_value(self, value): if value is True: return 'checked="checked"' else: return '' class SelectOption(Option): ''' make a selection from a list of valid string values. argument valid_range cannot be empty with this class. ''' option_template = r'''''' field_template = '''''' def _default(self): ''' we stipulate that valid_range is not empty. ''' try: return self.valid_range[0] except (TypeError, IndexError): raise OptionError, 'valid_range does not supply default' def _validate(self, value): '''' we enforce conversion to lowercase ''' return True, value.lower() def format_tag(self, value=None): value = value or self.default options = [] if not self.default in self.valid_range: # why am I doing this here? raise OptionError, 'invalid default' for option in self.valid_range: if option == value: selected = 'selected="selected"' else: selected = '' options.append(self.option_template % dict(option=option, selected=selected)) option_string = '\n'.join(options) tag = self.field_template % dict(options = option_string, key=self.key) return self.key, tag, self.form_text, self.help_text class TypeOption(Option): ''' coerces an input value to a type ''' _type = int # example _class_default = 0 form_tag_template = r'''''' def _validate(self, value): try: converted = self._type(value) return True, converted except ValueError: return False, value class IntOption(TypeOption): _type = int class FloatOption(TypeOption): _type = float class StringOption(TypeOption): _type = str class RangeOption(Option): ''' accept a string that can be parsed into one or more int ranges, such as 5-6,7-19 these should be converted into [(5,6),(7,19)] ''' outersep = ',' innersep = '-' form_tag_template = r'''''' def _validate(self, rawvalue): ranges = [] outerfrags = rawvalue.split(self.outersep) for frag in outerfrags: innerfrags = frag.split(self.innersep) if len(innerfrags) != 2: return False, rawvalue try: ranges.append((int(innerfrags[0]), int(innerfrags[1]))) except ValueError: return False, rawvalue return True, ranges class OptionParser(object): ''' collect and process options. the result will be contained in a dict. ''' def __init__(self): self._options = [] self._options_by_name = {} self._options_by_key = {} def append(self, option): if option.short_name in self._options_by_name: raise OptionError, "option name clash %s" % option.short_name if option.long_name in self._options_by_name: raise OptionError, "option name clash %s" % option_short_name self._options_by_name[option.short_name] = option.key self._options_by_name[option.long_name] = option.key self._options_by_key[option.key] = option # also maintain options ordered in a list self._options.append(option) def validKeys(self): ''' required by the web form front end ''' return self._option_by_key.keys() def option_values(self): ''' read current option values ''' option_dict = {} for option in self._options: option_dict[option.key] = option.value return option_dict def process_form_fields(self, fields): ''' process options received through the web form. we don't look at the cargo data here at all. what do we do about invalid options? puke? ignore? create a list of warnings and then ignore. ''' warnings = [] for key, value in fields.items(): option = self._options_by_key[key] if not option.validate_form_value(value): msg = 'Invalid value %s for option %s ignored' % (value, option.form_text) warnings.append(msg) return self.option_values(), warnings def process_cli(self, rawinput): ''' process input from the command line interface - assemble template strings for getopt and run getopt - pass the result back to each option ''' try: # accept lists or strings rawinput = rawinput.strip().split() except AttributeError: pass shorts, longs = self.format_for_getopt() opts, args = getopt.getopt(rawinput, shorts, longs) for optname, value in opts: key = self._options_by_name[optname.lstrip('-')] option = self._options_by_key[key] if not option.validate(value): msg = ["rejected value '%s' for option %s" % (value, optname)] msg.append('Option usage:') msg.extend(option.format_help()) raise OptionError, '\n'.join(msg) return self.option_values(), args def format_for_getopt(self): shorts = ''.join([option.short_getopt() for option in self._options]) longs = [option.long_getopt() for option in self._options] return shorts, longs def format_for_lua(self): ''' with lua, we use dumb option parsing. we only provide enough information for lua to distinguish between options with and without arguments. ''' bools = [opt for opt in self._options if isinstance(opt, BoolOption)] shorts = [nb.short_name for nb in bools] return ''.join(shorts) def format_help(self, indent=25, linewidth=70, separator=None): ''' just ask the options to render themselves ''' output = [] for option in self._options: output.extend(option.format_help(indent, linewidth)) if separator is not None: output.append(separator) return '\n'.join(output) def form_tags(self): ''' collect the html for each option ''' return [opt.format_tag() for opt in self._options] if __name__ == '__main__': # test it p = OptionParser() p.append(BoolOption( 'absolute', 'a', # default=True, help_text = 'not relative. what happens if we choose to use a really, really, really excessively long help text here?')) p.append(IntOption( 'count', 'c', default=5, valid_range=range(10), help_text="how many apples to buy")) p.append(StringOption( 'party', 'p', default="NDP", help_text="what party to choose")) p.append(FloatOption( 'diameter', 'd', default=3.14, help_text='how big it is')) p.append(StringOption( 'candy', 'n', default='chocolate')) rawinput = "-a -c 6 -p LP alpha beta gamma" options, args = p.process_cli(rawinput) print 'options', options print 'args', args print print p.format_help() print p.form_tags()