diff options
Diffstat (limited to 'third_party/py/gflags/gflags/argument_parser.py')
-rw-r--r-- | third_party/py/gflags/gflags/argument_parser.py | 480 |
1 files changed, 480 insertions, 0 deletions
diff --git a/third_party/py/gflags/gflags/argument_parser.py b/third_party/py/gflags/gflags/argument_parser.py new file mode 100644 index 0000000000..9f7262b231 --- /dev/null +++ b/third_party/py/gflags/gflags/argument_parser.py @@ -0,0 +1,480 @@ +#!/usr/bin/env python +# Copyright 2002 Google Inc. All Rights Reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Contains base classes used to parse and convert arguments. + +Instead of importing this module directly, it's preferable to import the +flags package and use the aliases defined at the package level. +""" + +import csv +import io +import string + +import six + +import _helpers + + +class _ArgumentParserCache(type): + """Metaclass used to cache and share argument parsers among flags.""" + + _instances = {} + + def __new__(mcs, name, bases, dct): + _helpers.define_both_methods(name, dct, 'Parse', 'parse') + _helpers.define_both_methods(name, dct, 'Type', 'flag_type') + _helpers.define_both_methods(name, dct, 'Convert', 'convert') + return type.__new__(mcs, name, bases, dct) + + def __call__(cls, *args, **kwargs): + """Returns an instance of the argument parser cls. + + This method overrides behavior of the __new__ methods in + all subclasses of ArgumentParser (inclusive). If an instance + for cls with the same set of arguments exists, this instance is + returned, otherwise a new instance is created. + + If any keyword arguments are defined, or the values in args + are not hashable, this method always returns a new instance of + cls. + + Args: + *args: Positional initializer arguments. + **kwargs: Initializer keyword arguments. + + Returns: + An instance of cls, shared or new. + """ + if kwargs: + return type.__call__(cls, *args, **kwargs) + else: + instances = cls._instances + key = (cls,) + tuple(args) + try: + return instances[key] + except KeyError: + # No cache entry for key exists, create a new one. + return instances.setdefault(key, type.__call__(cls, *args)) + except TypeError: + # An object in args cannot be hashed, always return + # a new instance. + return type.__call__(cls, *args) + + +class ArgumentParser(six.with_metaclass(_ArgumentParserCache, object)): + """Base class used to parse and convert arguments. + + The parse() method checks to make sure that the string argument is a + legal value and convert it to a native type. If the value cannot be + converted, it should throw a 'ValueError' exception with a human + readable explanation of why the value is illegal. + + Subclasses should also define a syntactic_help string which may be + presented to the user to describe the form of the legal values. + + Argument parser classes must be stateless, since instances are cached + and shared between flags. Initializer arguments are allowed, but all + member variables must be derived from initializer arguments only. + """ + + syntactic_help = '' + + def parse(self, argument): + """Parses the string argument and returns the native value. + + By default it returns its argument unmodified. + + Args: + argument: string argument passed in the commandline. + + Raises: + ValueError: Raised when it fails to parse the argument. + + Returns: + The parsed value in native type. + """ + return argument + + def flag_type(self): + """Returns a string representing the type of the flag.""" + return 'string' + + def _custom_xml_dom_elements(self, doc): # pylint: disable=unused-argument + """Returns a list of XML DOM elements to add additional flag information. + + Args: + doc: A minidom.Document, the DOM document it should create nodes from. + + Returns: + A list of minidom.Element. + """ + return [] + + +class _ArgumentSerializerMeta(type): + + def __new__(mcs, name, bases, dct): + _helpers.define_both_methods(name, dct, 'Serialize', 'serialize') + return type.__new__(mcs, name, bases, dct) + + +class ArgumentSerializer(six.with_metaclass(_ArgumentSerializerMeta, object)): + """Base class for generating string representations of a flag value.""" + + def serialize(self, value): + return _helpers.StrOrUnicode(value) + + +class NumericParser(ArgumentParser): + """Parser of numeric values. + + Parsed value may be bounded to a given upper and lower bound. + """ + + def is_outside_bounds(self, val): + return ((self.lower_bound is not None and val < self.lower_bound) or + (self.upper_bound is not None and val > self.upper_bound)) + + def parse(self, argument): + val = self.convert(argument) + if self.is_outside_bounds(val): + raise ValueError('%s is not %s' % (val, self.syntactic_help)) + return val + + def _custom_xml_dom_elements(self, doc): + elements = [] + if self.lower_bound is not None: + elements.append(_helpers.CreateXMLDOMElement( + doc, 'lower_bound', self.lower_bound)) + if self.upper_bound is not None: + elements.append(_helpers.CreateXMLDOMElement( + doc, 'upper_bound', self.upper_bound)) + return elements + + def convert(self, argument): + """Default implementation: always returns its argument unmodified.""" + return argument + + +class FloatParser(NumericParser): + """Parser of floating point values. + + Parsed value may be bounded to a given upper and lower bound. + """ + number_article = 'a' + number_name = 'number' + syntactic_help = ' '.join((number_article, number_name)) + + def __init__(self, lower_bound=None, upper_bound=None): + super(FloatParser, self).__init__() + self.lower_bound = lower_bound + self.upper_bound = upper_bound + sh = self.syntactic_help + if lower_bound is not None and upper_bound is not None: + sh = ('%s in the range [%s, %s]' % (sh, lower_bound, upper_bound)) + elif lower_bound == 0: + sh = 'a non-negative %s' % self.number_name + elif upper_bound == 0: + sh = 'a non-positive %s' % self.number_name + elif upper_bound is not None: + sh = '%s <= %s' % (self.number_name, upper_bound) + elif lower_bound is not None: + sh = '%s >= %s' % (self.number_name, lower_bound) + self.syntactic_help = sh + + def convert(self, argument): + """Converts argument to a float; raises ValueError on errors.""" + return float(argument) + + def flag_type(self): + return 'float' + + +class IntegerParser(NumericParser): + """Parser of an integer value. + + Parsed value may be bounded to a given upper and lower bound. + """ + number_article = 'an' + number_name = 'integer' + syntactic_help = ' '.join((number_article, number_name)) + + def __init__(self, lower_bound=None, upper_bound=None): + super(IntegerParser, self).__init__() + self.lower_bound = lower_bound + self.upper_bound = upper_bound + sh = self.syntactic_help + if lower_bound is not None and upper_bound is not None: + sh = ('%s in the range [%s, %s]' % (sh, lower_bound, upper_bound)) + elif lower_bound == 1: + sh = 'a positive %s' % self.number_name + elif upper_bound == -1: + sh = 'a negative %s' % self.number_name + elif lower_bound == 0: + sh = 'a non-negative %s' % self.number_name + elif upper_bound == 0: + sh = 'a non-positive %s' % self.number_name + elif upper_bound is not None: + sh = '%s <= %s' % (self.number_name, upper_bound) + elif lower_bound is not None: + sh = '%s >= %s' % (self.number_name, lower_bound) + self.syntactic_help = sh + + def convert(self, argument): + if isinstance(argument, str): + base = 10 + if len(argument) > 2 and argument[0] == '0': + if argument[1] == 'o': + base = 8 + elif argument[1] == 'x': + base = 16 + return int(argument, base) + else: + return int(argument) + + def flag_type(self): + return 'int' + + +class BooleanParser(ArgumentParser): + """Parser of boolean values.""" + + def convert(self, argument): + """Converts the argument to a boolean; raise ValueError on errors.""" + if isinstance(argument, str): + if argument.lower() in ['true', 't', '1']: + return True + elif argument.lower() in ['false', 'f', '0']: + return False + + bool_argument = bool(argument) + if argument == bool_argument: + # The argument is a valid boolean (True, False, 0, or 1), and not just + # something that always converts to bool (list, string, int, etc.). + return bool_argument + + raise ValueError('Non-boolean argument to boolean flag', argument) + + def parse(self, argument): + val = self.convert(argument) + return val + + def flag_type(self): + return 'bool' + + +class EnumParser(ArgumentParser): + """Parser of a string enum value (a string value from a given set). + + If enum_values (see below) is not specified, any string is allowed. + """ + + def __init__(self, enum_values=None, case_sensitive=True): + """Initialize EnumParser. + + Args: + enum_values: Array of values in the enum. + case_sensitive: Whether or not the enum is to be case-sensitive. + """ + super(EnumParser, self).__init__() + self.enum_values = enum_values + self.case_sensitive = case_sensitive + + def parse(self, argument): + """Determine validity of argument and return the correct element of enum. + + If self.enum_values is empty, then all arguments are valid and argument + will be returned. + + Otherwise, if argument matches an element in enum, then the first + matching element will be returned. + + Args: + argument: The supplied flag value. + + Returns: + The matching element from enum_values, or argument if enum_values is + empty. + + Raises: + ValueError: enum_values was non-empty, but argument didn't match + anything in enum. + """ + if not self.enum_values: + return argument + elif self.case_sensitive: + if argument not in self.enum_values: + raise ValueError('value should be one of <%s>' % + '|'.join(self.enum_values)) + else: + return argument + else: + if argument.upper() not in [value.upper() for value in self.enum_values]: + raise ValueError('value should be one of <%s>' % + '|'.join(self.enum_values)) + else: + return [value for value in self.enum_values + if value.upper() == argument.upper()][0] + + def flag_type(self): + return 'string enum' + + +class ListSerializer(ArgumentSerializer): + + def __init__(self, list_sep): + self.list_sep = list_sep + + def serialize(self, value): + return self.list_sep.join([_helpers.StrOrUnicode(x) for x in value]) + + +class CsvListSerializer(ArgumentSerializer): + + def __init__(self, list_sep): + self.list_sep = list_sep + + def serialize(self, value): + """Serialize a list as a string, if possible, or as a unicode string.""" + if six.PY2: + # In Python2 csv.writer doesn't accept unicode, so we convert to UTF-8. + output = io.BytesIO() + csv.writer(output).writerow([unicode(x).encode('utf-8') for x in value]) + serialized_value = output.getvalue().decode('utf-8').strip() + else: + # In Python3 csv.writer expects a text stream. + output = io.StringIO() + csv.writer(output).writerow([str(x) for x in value]) + serialized_value = output.getvalue().strip() + + # We need the returned value to be pure ascii or Unicodes so that + # when the xml help is generated they are usefully encodable. + return _helpers.StrOrUnicode(serialized_value) + + +class BaseListParser(ArgumentParser): + """Base class for a parser of lists of strings. + + To extend, inherit from this class; from the subclass __init__, call + + BaseListParser.__init__(self, token, name) + + where token is a character used to tokenize, and name is a description + of the separator. + """ + + def __init__(self, token=None, name=None): + assert name + super(BaseListParser, self).__init__() + self._token = token + self._name = name + self.syntactic_help = 'a %s separated list' % self._name + + def parse(self, argument): + if isinstance(argument, list): + return argument + elif not argument: + return [] + else: + return [s.strip() for s in argument.split(self._token)] + + def flag_type(self): + return '%s separated list of strings' % self._name + + +class ListParser(BaseListParser): + """Parser for a comma-separated list of strings.""" + + def __init__(self): + BaseListParser.__init__(self, ',', 'comma') + + def parse(self, argument): + """Override to support full CSV syntax.""" + if isinstance(argument, list): + return argument + elif not argument: + return [] + else: + try: + return [s.strip() for s in list(csv.reader([argument], strict=True))[0]] + except csv.Error as e: + # Provide a helpful report for case like + # --listflag="$(printf 'hello,\nworld')" + # IOW, list flag values containing naked newlines. This error + # was previously "reported" by allowing csv.Error to + # propagate. + raise ValueError('Unable to parse the value %r as a %s: %s' + % (argument, self.flag_type(), e)) + + def _custom_xml_dom_elements(self, doc): + elements = super(ListParser, self)._custom_xml_dom_elements(doc) + elements.append(_helpers.CreateXMLDOMElement( + doc, 'list_separator', repr(','))) + return elements + + +class WhitespaceSeparatedListParser(BaseListParser): + """Parser for a whitespace-separated list of strings.""" + + def __init__(self, comma_compat=False): + """Initializer. + + Args: + comma_compat: bool - Whether to support comma as an additional separator. + If false then only whitespace is supported. This is intended only for + backwards compatibility with flags that used to be comma-separated. + """ + self._comma_compat = comma_compat + name = 'whitespace or comma' if self._comma_compat else 'whitespace' + BaseListParser.__init__(self, None, name) + + def parse(self, argument): + """Override to support comma compatibility.""" + if isinstance(argument, list): + return argument + elif not argument: + return [] + else: + if self._comma_compat: + argument = argument.replace(',', ' ') + return argument.split() + + def _custom_xml_dom_elements(self, doc): + elements = super(WhitespaceSeparatedListParser, self + )._custom_xml_dom_elements(doc) + separators = list(string.whitespace) + if self._comma_compat: + separators.append(',') + separators.sort() + for sep_char in separators: + elements.append(_helpers.CreateXMLDOMElement( + doc, 'list_separator', repr(sep_char))) + return elements |