I am done

This commit is contained in:
2024-10-30 22:14:35 +01:00
parent 720dc28c09
commit 40e2a747cf
36901 changed files with 5011519 additions and 0 deletions

View File

@ -0,0 +1,225 @@
# Copyright 2017 The Abseil Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""This package is used to define and parse command line flags.
This package defines a *distributed* flag-definition policy: rather than
an application having to define all flags in or near main(), each Python
module defines flags that are useful to it. When one Python module
imports another, it gains access to the other's flags. (This is
implemented by having all modules share a common, global registry object
containing all the flag information.)
Flags are defined through the use of one of the DEFINE_xxx functions.
The specific function used determines how the flag is parsed, checked,
and optionally type-converted, when it's seen on the command line.
"""
import getopt
import os
import re
import sys
import types
import warnings
from absl.flags import _argument_parser
from absl.flags import _defines
from absl.flags import _exceptions
from absl.flags import _flag
from absl.flags import _flagvalues
from absl.flags import _helpers
from absl.flags import _validators
__all__ = (
'DEFINE',
'DEFINE_flag',
'DEFINE_string',
'DEFINE_boolean',
'DEFINE_bool',
'DEFINE_float',
'DEFINE_integer',
'DEFINE_enum',
'DEFINE_enum_class',
'DEFINE_list',
'DEFINE_spaceseplist',
'DEFINE_multi',
'DEFINE_multi_string',
'DEFINE_multi_integer',
'DEFINE_multi_float',
'DEFINE_multi_enum',
'DEFINE_multi_enum_class',
'DEFINE_alias',
# Flag validators.
'register_validator',
'validator',
'register_multi_flags_validator',
'multi_flags_validator',
'mark_flag_as_required',
'mark_flags_as_required',
'mark_flags_as_mutual_exclusive',
'mark_bool_flags_as_mutual_exclusive',
# Flag modifiers.
'set_default',
'override_value',
# Key flag related functions.
'declare_key_flag',
'adopt_module_key_flags',
'disclaim_key_flags',
# Module exceptions.
'Error',
'CantOpenFlagFileError',
'DuplicateFlagError',
'IllegalFlagValueError',
'UnrecognizedFlagError',
'UnparsedFlagAccessError',
'ValidationError',
'FlagNameConflictsWithMethodError',
# Public classes.
'Flag',
'BooleanFlag',
'EnumFlag',
'EnumClassFlag',
'MultiFlag',
'MultiEnumClassFlag',
'FlagHolder',
'FlagValues',
'ArgumentParser',
'BooleanParser',
'EnumParser',
'EnumClassParser',
'ArgumentSerializer',
'FloatParser',
'IntegerParser',
'BaseListParser',
'ListParser',
'ListSerializer',
'EnumClassListSerializer',
'CsvListSerializer',
'WhitespaceSeparatedListParser',
'EnumClassSerializer',
# Helper functions.
'get_help_width',
'text_wrap',
'flag_dict_to_args',
'doc_to_help',
# The global FlagValues instance.
'FLAGS',
)
# Initialize the FLAGS_MODULE as early as possible.
# It's only used by adopt_module_key_flags to take SPECIAL_FLAGS into account.
_helpers.FLAGS_MODULE = sys.modules[__name__]
# Add current module to disclaimed module ids.
_helpers.disclaim_module_ids.add(id(sys.modules[__name__]))
# DEFINE functions. They are explained in more details in the module doc string.
# pylint: disable=invalid-name
DEFINE = _defines.DEFINE
DEFINE_flag = _defines.DEFINE_flag
DEFINE_string = _defines.DEFINE_string
DEFINE_boolean = _defines.DEFINE_boolean
DEFINE_bool = DEFINE_boolean # Match C++ API.
DEFINE_float = _defines.DEFINE_float
DEFINE_integer = _defines.DEFINE_integer
DEFINE_enum = _defines.DEFINE_enum
DEFINE_enum_class = _defines.DEFINE_enum_class
DEFINE_list = _defines.DEFINE_list
DEFINE_spaceseplist = _defines.DEFINE_spaceseplist
DEFINE_multi = _defines.DEFINE_multi
DEFINE_multi_string = _defines.DEFINE_multi_string
DEFINE_multi_integer = _defines.DEFINE_multi_integer
DEFINE_multi_float = _defines.DEFINE_multi_float
DEFINE_multi_enum = _defines.DEFINE_multi_enum
DEFINE_multi_enum_class = _defines.DEFINE_multi_enum_class
DEFINE_alias = _defines.DEFINE_alias
# pylint: enable=invalid-name
# Flag validators.
register_validator = _validators.register_validator
validator = _validators.validator
register_multi_flags_validator = _validators.register_multi_flags_validator
multi_flags_validator = _validators.multi_flags_validator
mark_flag_as_required = _validators.mark_flag_as_required
mark_flags_as_required = _validators.mark_flags_as_required
mark_flags_as_mutual_exclusive = _validators.mark_flags_as_mutual_exclusive
mark_bool_flags_as_mutual_exclusive = _validators.mark_bool_flags_as_mutual_exclusive
# Flag modifiers.
set_default = _defines.set_default
override_value = _defines.override_value
# Key flag related functions.
declare_key_flag = _defines.declare_key_flag
adopt_module_key_flags = _defines.adopt_module_key_flags
disclaim_key_flags = _defines.disclaim_key_flags
# Module exceptions.
# pylint: disable=invalid-name
Error = _exceptions.Error
CantOpenFlagFileError = _exceptions.CantOpenFlagFileError
DuplicateFlagError = _exceptions.DuplicateFlagError
IllegalFlagValueError = _exceptions.IllegalFlagValueError
UnrecognizedFlagError = _exceptions.UnrecognizedFlagError
UnparsedFlagAccessError = _exceptions.UnparsedFlagAccessError
ValidationError = _exceptions.ValidationError
FlagNameConflictsWithMethodError = _exceptions.FlagNameConflictsWithMethodError
# Public classes.
Flag = _flag.Flag
BooleanFlag = _flag.BooleanFlag
EnumFlag = _flag.EnumFlag
EnumClassFlag = _flag.EnumClassFlag
MultiFlag = _flag.MultiFlag
MultiEnumClassFlag = _flag.MultiEnumClassFlag
FlagHolder = _flagvalues.FlagHolder
FlagValues = _flagvalues.FlagValues
ArgumentParser = _argument_parser.ArgumentParser
BooleanParser = _argument_parser.BooleanParser
EnumParser = _argument_parser.EnumParser
EnumClassParser = _argument_parser.EnumClassParser
ArgumentSerializer = _argument_parser.ArgumentSerializer
FloatParser = _argument_parser.FloatParser
IntegerParser = _argument_parser.IntegerParser
BaseListParser = _argument_parser.BaseListParser
ListParser = _argument_parser.ListParser
ListSerializer = _argument_parser.ListSerializer
EnumClassListSerializer = _argument_parser.EnumClassListSerializer
CsvListSerializer = _argument_parser.CsvListSerializer
WhitespaceSeparatedListParser = _argument_parser.WhitespaceSeparatedListParser
EnumClassSerializer = _argument_parser.EnumClassSerializer
# pylint: enable=invalid-name
# Helper functions.
get_help_width = _helpers.get_help_width
text_wrap = _helpers.text_wrap
flag_dict_to_args = _helpers.flag_dict_to_args
doc_to_help = _helpers.doc_to_help
# Special flags.
_helpers.SPECIAL_FLAGS = FlagValues()
DEFINE_string(
'flagfile', '',
'Insert flag definitions from the given file into the command line.',
_helpers.SPECIAL_FLAGS) # pytype: disable=wrong-arg-types
DEFINE_string('undefok', '',
'comma-separated list of flag names that it is okay to specify '
'on the command line even if the program does not define a flag '
'with that name. IMPORTANT: flags in this list that have '
'arguments MUST use the --flag=value format.',
_helpers.SPECIAL_FLAGS) # pytype: disable=wrong-arg-types
#: The global FlagValues instance.
FLAGS = _flagvalues.FLAGS

View File

@ -0,0 +1,638 @@
# Copyright 2017 The Abseil Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Contains base classes used to parse and convert arguments.
Do NOT import this module directly. Import the flags package and use the
aliases defined at the package level instead.
"""
import collections
import csv
import enum
import io
import string
from typing import Generic, List, Iterable, Optional, Sequence, Text, Type, TypeVar, Union
from xml.dom import minidom
from absl.flags import _helpers
_T = TypeVar('_T')
_ET = TypeVar('_ET', bound=enum.Enum)
_N = TypeVar('_N', int, float)
def _is_integer_type(instance):
"""Returns True if instance is an integer, and not a bool."""
return (isinstance(instance, int) and
not isinstance(instance, bool))
class _ArgumentParserCache(type):
"""Metaclass used to cache and share argument parsers among flags."""
_instances = {}
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(Generic[_T], metaclass=_ArgumentParserCache):
"""Base class used to parse and convert arguments.
The :meth:`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: Text = ''
def parse(self, argument: Text) -> Optional[_T]:
"""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.
TypeError: Raised when the argument has the wrong type.
Returns:
The parsed value in native type.
"""
if not isinstance(argument, str):
raise TypeError('flag value must be a string, found "{}"'.format(
type(argument)))
return argument
def flag_type(self) -> Text:
"""Returns a string representing the type of the flag."""
return 'string'
def _custom_xml_dom_elements(
self, doc: minidom.Document
) -> List[minidom.Element]:
"""Returns a list of minidom.Element to add additional flag information.
Args:
doc: minidom.Document, the DOM document it should create nodes from.
"""
del doc # Unused.
return []
class ArgumentSerializer(Generic[_T]):
"""Base class for generating string representations of a flag value."""
def serialize(self, value: _T) -> Text:
"""Returns a serialized string of the value."""
return str(value)
class NumericParser(ArgumentParser[_N]):
"""Parser of numeric values.
Parsed value may be bounded to a given upper and lower bound.
"""
lower_bound: Optional[_N]
upper_bound: Optional[_N]
def is_outside_bounds(self, val: _N) -> bool:
"""Returns whether the value is outside the bounds or not."""
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: Text) -> _N:
"""See base class."""
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: minidom.Document
) -> List[minidom.Element]:
elements = []
if self.lower_bound is not None:
elements.append(_helpers.create_xml_dom_element(
doc, 'lower_bound', self.lower_bound))
if self.upper_bound is not None:
elements.append(_helpers.create_xml_dom_element(
doc, 'upper_bound', self.upper_bound))
return elements
def convert(self, argument: Text) -> _N:
"""Returns the correct numeric value of argument.
Subclass must implement this method, and raise TypeError if argument is not
string or has the right numeric type.
Args:
argument: string argument passed in the commandline, or the numeric type.
Raises:
TypeError: Raised when argument is not a string or the right numeric type.
ValueError: Raised when failed to convert argument to the numeric value.
"""
raise NotImplementedError
class FloatParser(NumericParser[float]):
"""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: Optional[float] = None,
upper_bound: Optional[float] = None,
) -> 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: Union[int, float, str]) -> float:
"""Returns the float value of argument."""
if (_is_integer_type(argument) or isinstance(argument, float) or
isinstance(argument, str)):
return float(argument)
else:
raise TypeError(
'Expect argument to be a string, int, or float, found {}'.format(
type(argument)))
def flag_type(self) -> Text:
"""See base class."""
return 'float'
class IntegerParser(NumericParser[int]):
"""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: Optional[int] = None, upper_bound: Optional[int] = None
) -> 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: Union[int, Text]) -> int:
"""Returns the int value of argument."""
if _is_integer_type(argument):
return argument
elif 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:
raise TypeError('Expect argument to be a string or int, found {}'.format(
type(argument)))
def flag_type(self) -> Text:
"""See base class."""
return 'int'
class BooleanParser(ArgumentParser[bool]):
"""Parser of boolean values."""
def parse(self, argument: Union[Text, int]) -> bool:
"""See base class."""
if isinstance(argument, str):
if argument.lower() in ('true', 't', '1'):
return True
elif argument.lower() in ('false', 'f', '0'):
return False
else:
raise ValueError('Non-boolean argument to boolean flag', argument)
elif isinstance(argument, int):
# Only allow bool or integer 0, 1.
# Note that float 1.0 == True, 0.0 == False.
bool_value = bool(argument)
if argument == bool_value:
return bool_value
else:
raise ValueError('Non-boolean argument to boolean flag', argument)
raise TypeError('Non-boolean argument to boolean flag', argument)
def flag_type(self) -> Text:
"""See base class."""
return 'bool'
class EnumParser(ArgumentParser[Text]):
"""Parser of a string enum value (a string value from a given set)."""
def __init__(
self, enum_values: Iterable[Text], case_sensitive: bool = True
) -> None:
"""Initializes EnumParser.
Args:
enum_values: [str], a non-empty list of string values in the enum.
case_sensitive: bool, whether or not the enum is to be case-sensitive.
Raises:
ValueError: When enum_values is empty.
"""
if not enum_values:
raise ValueError(
'enum_values cannot be empty, found "{}"'.format(enum_values))
if isinstance(enum_values, str):
raise ValueError(
'enum_values cannot be a str, found "{}"'.format(enum_values)
)
super(EnumParser, self).__init__()
self.enum_values = list(enum_values)
self.case_sensitive = case_sensitive
def parse(self, argument: Text) -> Text:
"""Determines validity of argument and returns the correct element of enum.
Args:
argument: str, the supplied flag value.
Returns:
The first matching element from enum_values.
Raises:
ValueError: Raised when argument didn't match anything in enum.
"""
if 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) -> Text:
"""See base class."""
return 'string enum'
class EnumClassParser(ArgumentParser[_ET]):
"""Parser of an Enum class member."""
def __init__(
self, enum_class: Type[_ET], case_sensitive: bool = True
) -> None:
"""Initializes EnumParser.
Args:
enum_class: class, the Enum class with all possible flag values.
case_sensitive: bool, whether or not the enum is to be case-sensitive. If
False, all member names must be unique when case is ignored.
Raises:
TypeError: When enum_class is not a subclass of Enum.
ValueError: When enum_class is empty.
"""
if not issubclass(enum_class, enum.Enum):
raise TypeError('{} is not a subclass of Enum.'.format(enum_class))
if not enum_class.__members__:
raise ValueError('enum_class cannot be empty, but "{}" is empty.'
.format(enum_class))
if not case_sensitive:
members = collections.Counter(
name.lower() for name in enum_class.__members__)
duplicate_keys = {
member for member, count in members.items() if count > 1
}
if duplicate_keys:
raise ValueError(
'Duplicate enum values for {} using case_sensitive=False'.format(
duplicate_keys))
super(EnumClassParser, self).__init__()
self.enum_class = enum_class
self._case_sensitive = case_sensitive
if case_sensitive:
self._member_names = tuple(enum_class.__members__)
else:
self._member_names = tuple(
name.lower() for name in enum_class.__members__)
@property
def member_names(self) -> Sequence[Text]:
"""The accepted enum names, in lowercase if not case sensitive."""
return self._member_names
def parse(self, argument: Union[_ET, Text]) -> _ET:
"""Determines validity of argument and returns the correct element of enum.
Args:
argument: str or Enum class member, the supplied flag value.
Returns:
The first matching Enum class member in Enum class.
Raises:
ValueError: Raised when argument didn't match anything in enum.
"""
if isinstance(argument, self.enum_class):
return argument # pytype: disable=bad-return-type
elif not isinstance(argument, str):
raise ValueError(
'{} is not an enum member or a name of a member in {}'.format(
argument, self.enum_class))
key = EnumParser(
self._member_names, case_sensitive=self._case_sensitive).parse(argument)
if self._case_sensitive:
return self.enum_class[key]
else:
# If EnumParser.parse() return a value, we're guaranteed to find it
# as a member of the class
return next(value for name, value in self.enum_class.__members__.items()
if name.lower() == key.lower())
def flag_type(self) -> Text:
"""See base class."""
return 'enum class'
class ListSerializer(Generic[_T], ArgumentSerializer[List[_T]]):
def __init__(self, list_sep: Text) -> None:
self.list_sep = list_sep
def serialize(self, value: List[_T]) -> Text:
"""See base class."""
return self.list_sep.join([str(x) for x in value])
class EnumClassListSerializer(ListSerializer[_ET]):
"""A serializer for :class:`MultiEnumClass` flags.
This serializer simply joins the output of `EnumClassSerializer` using a
provided separator.
"""
def __init__(self, list_sep: Text, **kwargs) -> None:
"""Initializes EnumClassListSerializer.
Args:
list_sep: String to be used as a separator when serializing
**kwargs: Keyword arguments to the `EnumClassSerializer` used to serialize
individual values.
"""
super(EnumClassListSerializer, self).__init__(list_sep)
self._element_serializer = EnumClassSerializer(**kwargs)
def serialize(self, value: Union[_ET, List[_ET]]) -> Text:
"""See base class."""
if isinstance(value, list):
return self.list_sep.join(
self._element_serializer.serialize(x) for x in value)
else:
return self._element_serializer.serialize(value)
class CsvListSerializer(ListSerializer[Text]):
def serialize(self, value: List[Text]) -> Text:
"""Serializes a list as a CSV string or unicode."""
output = io.StringIO()
writer = csv.writer(output, delimiter=self.list_sep)
writer.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 str(serialized_value)
class EnumClassSerializer(ArgumentSerializer[_ET]):
"""Class for generating string representations of an enum class flag value."""
def __init__(self, lowercase: bool) -> None:
"""Initializes EnumClassSerializer.
Args:
lowercase: If True, enum member names are lowercased during serialization.
"""
self._lowercase = lowercase
def serialize(self, value: _ET) -> Text:
"""Returns a serialized string of the Enum class value."""
as_string = str(value.name)
return as_string.lower() if self._lowercase else as_string
class BaseListParser(ArgumentParser):
"""Base class for a parser of lists of strings.
To extend, inherit from this class; from the subclass ``__init__``, call::
super().__init__(token, name)
where token is a character used to tokenize, and name is a description
of the separator.
"""
def __init__(
self, token: Optional[Text] = None, name: Optional[Text] = None
) -> 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: Text) -> List[Text]:
"""See base class."""
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) -> Text:
"""See base class."""
return '%s separated list of strings' % self._name
class ListParser(BaseListParser):
"""Parser for a comma-separated list of strings."""
def __init__(self) -> None:
super(ListParser, self).__init__(',', 'comma')
def parse(self, argument: Union[Text, List[Text]]) -> List[Text]:
"""Parses argument as comma-separated list of strings."""
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: minidom.Document
) -> List[minidom.Element]:
elements = super(ListParser, self)._custom_xml_dom_elements(doc)
elements.append(_helpers.create_xml_dom_element(
doc, 'list_separator', repr(',')))
return elements
class WhitespaceSeparatedListParser(BaseListParser):
"""Parser for a whitespace-separated list of strings."""
def __init__(self, comma_compat: bool = False) -> None:
"""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'
super(WhitespaceSeparatedListParser, self).__init__(None, name)
def parse(self, argument: Union[Text, List[Text]]) -> List[Text]:
"""Parses argument as whitespace-separated list of strings.
It also parses argument as comma-separated list of strings if requested.
Args:
argument: string argument passed in the commandline.
Returns:
[str], the parsed flag value.
"""
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: minidom.Document
) -> List[minidom.Element]:
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.create_xml_dom_element(
doc, 'list_separator', repr(sep_char)))
return elements

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,108 @@
# Copyright 2017 The Abseil Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Exception classes in ABSL flags library.
Do NOT import this module directly. Import the flags package and use the
aliases defined at the package level instead.
"""
import sys
from absl.flags import _helpers
_helpers.disclaim_module_ids.add(id(sys.modules[__name__]))
class Error(Exception):
"""The base class for all flags errors."""
class CantOpenFlagFileError(Error):
"""Raised when flagfile fails to open.
E.g. the file doesn't exist, or has wrong permissions.
"""
class DuplicateFlagError(Error):
"""Raised if there is a flag naming conflict."""
@classmethod
def from_flag(cls, flagname, flag_values, other_flag_values=None):
"""Creates a DuplicateFlagError by providing flag name and values.
Args:
flagname: str, the name of the flag being redefined.
flag_values: :class:`FlagValues`, the FlagValues instance containing the
first definition of flagname.
other_flag_values: :class:`FlagValues`, if it is not None, it should be
the FlagValues object where the second definition of flagname occurs.
If it is None, we assume that we're being called when attempting to
create the flag a second time, and we use the module calling this one
as the source of the second definition.
Returns:
An instance of DuplicateFlagError.
"""
first_module = flag_values.find_module_defining_flag(
flagname, default='<unknown>')
if other_flag_values is None:
second_module = _helpers.get_calling_module()
else:
second_module = other_flag_values.find_module_defining_flag(
flagname, default='<unknown>')
flag_summary = flag_values[flagname].help
msg = ("The flag '%s' is defined twice. First from %s, Second from %s. "
"Description from first occurrence: %s") % (
flagname, first_module, second_module, flag_summary)
return cls(msg)
class IllegalFlagValueError(Error):
"""Raised when the flag command line argument is illegal."""
class UnrecognizedFlagError(Error):
"""Raised when a flag is unrecognized.
Attributes:
flagname: str, the name of the unrecognized flag.
flagvalue: The value of the flag, empty if the flag is not defined.
"""
def __init__(self, flagname, flagvalue='', suggestions=None):
self.flagname = flagname
self.flagvalue = flagvalue
if suggestions:
# Space before the question mark is intentional to not include it in the
# selection when copy-pasting the suggestion from (some) terminals.
tip = '. Did you mean: %s ?' % ', '.join(suggestions)
else:
tip = ''
super(UnrecognizedFlagError, self).__init__(
'Unknown command line flag \'%s\'%s' % (flagname, tip))
class UnparsedFlagAccessError(Error):
"""Raised when accessing the flag value from unparsed :class:`FlagValues`."""
class ValidationError(Error):
"""Raised when flag validator constraint is not satisfied."""
class FlagNameConflictsWithMethodError(Error):
"""Raised when a flag name conflicts with :class:`FlagValues` methods."""

View File

@ -0,0 +1,556 @@
# Copyright 2017 The Abseil Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Contains Flag class - information about single command-line flag.
Do NOT import this module directly. Import the flags package and use the
aliases defined at the package level instead.
"""
from collections import abc
import copy
import enum
import functools
from typing import Any, Dict, Generic, Iterable, List, Optional, Text, Type, TypeVar, Union
from xml.dom import minidom
from absl.flags import _argument_parser
from absl.flags import _exceptions
from absl.flags import _helpers
_T = TypeVar('_T')
_ET = TypeVar('_ET', bound=enum.Enum)
@functools.total_ordering
class Flag(Generic[_T]):
"""Information about a command-line flag.
Attributes:
name: the name for this flag
default: the default value for this flag
default_unparsed: the unparsed default value for this flag.
default_as_str: default value as repr'd string, e.g., "'true'"
(or None)
value: the most recent parsed value of this flag set by :meth:`parse`
help: a help string or None if no help is available
short_name: the single letter alias for this flag (or None)
boolean: if 'true', this flag does not accept arguments
present: true if this flag was parsed from command line flags
parser: an :class:`~absl.flags.ArgumentParser` object
serializer: an ArgumentSerializer object
allow_override: the flag may be redefined without raising an error,
and newly defined flag overrides the old one.
allow_override_cpp: use the flag from C++ if available the flag
definition is replaced by the C++ flag after init
allow_hide_cpp: use the Python flag despite having a C++ flag with
the same name (ignore the C++ flag)
using_default_value: the flag value has not been set by user
allow_overwrite: the flag may be parsed more than once without
raising an error, the last set value will be used
allow_using_method_names: whether this flag can be defined even if
it has a name that conflicts with a FlagValues method.
validators: list of the flag validators.
The only public method of a ``Flag`` object is :meth:`parse`, but it is
typically only called by a :class:`~absl.flags.FlagValues` object. The
:meth:`parse` method is a thin wrapper around the
:meth:`ArgumentParser.parse()<absl.flags.ArgumentParser.parse>` method. The
parsed value is saved in ``.value``, and the ``.present`` attribute is
updated. If this flag was already present, an Error is raised.
:meth:`parse` is also called during ``__init__`` to parse the default value
and initialize the ``.value`` attribute. This enables other python modules to
safely use flags even if the ``__main__`` module neglects to parse the
command line arguments. The ``.present`` attribute is cleared after
``__init__`` parsing. If the default value is set to ``None``, then the
``__init__`` parsing step is skipped and the ``.value`` attribute is
initialized to None.
Note: The default value is also presented to the user in the help
string, so it is important that it be a legal value for this flag.
"""
# NOTE: pytype doesn't find defaults without this.
default: Optional[_T]
default_as_str: Optional[Text]
default_unparsed: Union[Optional[_T], Text]
def __init__(
self,
parser: _argument_parser.ArgumentParser[_T],
serializer: Optional[_argument_parser.ArgumentSerializer[_T]],
name: Text,
default: Union[Optional[_T], Text],
help_string: Optional[Text],
short_name: Optional[Text] = None,
boolean: bool = False,
allow_override: bool = False,
allow_override_cpp: bool = False,
allow_hide_cpp: bool = False,
allow_overwrite: bool = True,
allow_using_method_names: bool = False,
) -> None:
self.name = name
if not help_string:
help_string = '(no help available)'
self.help = help_string
self.short_name = short_name
self.boolean = boolean
self.present = 0
self.parser = parser
self.serializer = serializer
self.allow_override = allow_override
self.allow_override_cpp = allow_override_cpp
self.allow_hide_cpp = allow_hide_cpp
self.allow_overwrite = allow_overwrite
self.allow_using_method_names = allow_using_method_names
self.using_default_value = True
self._value = None
self.validators = []
if self.allow_hide_cpp and self.allow_override_cpp:
raise _exceptions.Error(
"Can't have both allow_hide_cpp (means use Python flag) and "
'allow_override_cpp (means use C++ flag after InitGoogle)')
self._set_default(default)
@property
def value(self) -> Optional[_T]:
return self._value
@value.setter
def value(self, value: Optional[_T]):
self._value = value
def __hash__(self):
return hash(id(self))
def __eq__(self, other):
return self is other
def __lt__(self, other):
if isinstance(other, Flag):
return id(self) < id(other)
return NotImplemented
def __bool__(self):
raise TypeError('A Flag instance would always be True. '
'Did you mean to test the `.value` attribute?')
def __getstate__(self):
raise TypeError("can't pickle Flag objects")
def __copy__(self):
raise TypeError('%s does not support shallow copies. '
'Use copy.deepcopy instead.' % type(self).__name__)
def __deepcopy__(self, memo: Dict[int, Any]) -> 'Flag[_T]':
result = object.__new__(type(self))
result.__dict__ = copy.deepcopy(self.__dict__, memo)
return result
def _get_parsed_value_as_string(self, value: Optional[_T]) -> Optional[Text]:
"""Returns parsed flag value as string."""
if value is None:
return None
if self.serializer:
return repr(self.serializer.serialize(value))
if self.boolean:
if value:
return repr('true')
else:
return repr('false')
return repr(str(value))
def parse(self, argument: Union[Text, Optional[_T]]) -> None:
"""Parses string and sets flag value.
Args:
argument: str or the correct flag value type, argument to be parsed.
"""
if self.present and not self.allow_overwrite:
raise _exceptions.IllegalFlagValueError(
'flag --%s=%s: already defined as %s' % (
self.name, argument, self.value))
self.value = self._parse(argument)
self.present += 1
def _parse(self, argument: Union[Text, _T]) -> Optional[_T]:
"""Internal parse function.
It returns the parsed value, and does not modify class states.
Args:
argument: str or the correct flag value type, argument to be parsed.
Returns:
The parsed value.
"""
try:
return self.parser.parse(argument)
except (TypeError, ValueError) as e: # Recast as IllegalFlagValueError.
raise _exceptions.IllegalFlagValueError(
'flag --%s=%s: %s' % (self.name, argument, e))
def unparse(self) -> None:
self.value = self.default
self.using_default_value = True
self.present = 0
def serialize(self) -> Text:
"""Serializes the flag."""
return self._serialize(self.value)
def _serialize(self, value: Optional[_T]) -> Text:
"""Internal serialize function."""
if value is None:
return ''
if self.boolean:
if value:
return '--%s' % self.name
else:
return '--no%s' % self.name
else:
if not self.serializer:
raise _exceptions.Error(
'Serializer not present for flag %s' % self.name)
return '--%s=%s' % (self.name, self.serializer.serialize(value))
def _set_default(self, value: Union[Optional[_T], Text]) -> None:
"""Changes the default value (and current value too) for this Flag."""
self.default_unparsed = value
if value is None:
self.default = None
else:
self.default = self._parse_from_default(value)
self.default_as_str = self._get_parsed_value_as_string(self.default)
if self.using_default_value:
self.value = self.default
# This is split out so that aliases can skip regular parsing of the default
# value.
def _parse_from_default(self, value: Union[Text, _T]) -> Optional[_T]:
return self._parse(value)
def flag_type(self) -> Text:
"""Returns a str that describes the type of the flag.
NOTE: we use strings, and not the types.*Type constants because
our flags can have more exotic types, e.g., 'comma separated list
of strings', 'whitespace separated list of strings', etc.
"""
return self.parser.flag_type()
def _create_xml_dom_element(
self, doc: minidom.Document, module_name: str, is_key: bool = False
) -> minidom.Element:
"""Returns an XML element that contains this flag's information.
This is information that is relevant to all flags (e.g., name,
meaning, etc.). If you defined a flag that has some other pieces of
info, then please override _ExtraXMLInfo.
Please do NOT override this method.
Args:
doc: minidom.Document, the DOM document it should create nodes from.
module_name: str,, the name of the module that defines this flag.
is_key: boolean, True iff this flag is key for main module.
Returns:
A minidom.Element instance.
"""
element = doc.createElement('flag')
if is_key:
element.appendChild(_helpers.create_xml_dom_element(doc, 'key', 'yes'))
element.appendChild(_helpers.create_xml_dom_element(
doc, 'file', module_name))
# Adds flag features that are relevant for all flags.
element.appendChild(_helpers.create_xml_dom_element(doc, 'name', self.name))
if self.short_name:
element.appendChild(_helpers.create_xml_dom_element(
doc, 'short_name', self.short_name))
if self.help:
element.appendChild(_helpers.create_xml_dom_element(
doc, 'meaning', self.help))
# The default flag value can either be represented as a string like on the
# command line, or as a Python object. We serialize this value in the
# latter case in order to remain consistent.
if self.serializer and not isinstance(self.default, str):
if self.default is not None:
default_serialized = self.serializer.serialize(self.default)
else:
default_serialized = ''
else:
default_serialized = self.default
element.appendChild(_helpers.create_xml_dom_element(
doc, 'default', default_serialized))
value_serialized = self._serialize_value_for_xml(self.value)
element.appendChild(_helpers.create_xml_dom_element(
doc, 'current', value_serialized))
element.appendChild(_helpers.create_xml_dom_element(
doc, 'type', self.flag_type()))
# Adds extra flag features this flag may have.
for e in self._extra_xml_dom_elements(doc):
element.appendChild(e)
return element
def _serialize_value_for_xml(self, value: Optional[_T]) -> Any:
"""Returns the serialized value, for use in an XML help text."""
return value
def _extra_xml_dom_elements(
self, doc: minidom.Document
) -> List[minidom.Element]:
"""Returns extra info about this flag in XML.
"Extra" means "not already included by _create_xml_dom_element above."
Args:
doc: minidom.Document, the DOM document it should create nodes from.
Returns:
A list of minidom.Element.
"""
# Usually, the parser knows the extra details about the flag, so
# we just forward the call to it.
return self.parser._custom_xml_dom_elements(doc) # pylint: disable=protected-access
class BooleanFlag(Flag[bool]):
"""Basic boolean flag.
Boolean flags do not take any arguments, and their value is either
``True`` (1) or ``False`` (0). The false value is specified on the command
line by prepending the word ``'no'`` to either the long or the short flag
name.
For example, if a Boolean flag was created whose long name was
``'update'`` and whose short name was ``'x'``, then this flag could be
explicitly unset through either ``--noupdate`` or ``--nox``.
"""
def __init__(
self,
name: Text,
default: Union[Optional[bool], Text],
help: Optional[Text], # pylint: disable=redefined-builtin
short_name: Optional[Text] = None,
**args
) -> None:
p = _argument_parser.BooleanParser()
super(BooleanFlag, self).__init__(
p, None, name, default, help, short_name, True, **args
)
class EnumFlag(Flag[Text]):
"""Basic enum flag; its value can be any string from list of enum_values."""
def __init__(
self,
name: Text,
default: Optional[Text],
help: Optional[Text], # pylint: disable=redefined-builtin
enum_values: Iterable[Text],
short_name: Optional[Text] = None,
case_sensitive: bool = True,
**args
):
p = _argument_parser.EnumParser(enum_values, case_sensitive)
g = _argument_parser.ArgumentSerializer()
super(EnumFlag, self).__init__(
p, g, name, default, help, short_name, **args)
# NOTE: parser should be typed EnumParser but the constructor
# restricts the available interface to ArgumentParser[str].
self.parser = p
self.help = '<%s>: %s' % ('|'.join(p.enum_values), self.help)
def _extra_xml_dom_elements(
self, doc: minidom.Document
) -> List[minidom.Element]:
elements = []
for enum_value in self.parser.enum_values:
elements.append(_helpers.create_xml_dom_element(
doc, 'enum_value', enum_value))
return elements
class EnumClassFlag(Flag[_ET]):
"""Basic enum flag; its value is an enum class's member."""
def __init__(
self,
name: Text,
default: Union[Optional[_ET], Text],
help: Optional[Text], # pylint: disable=redefined-builtin
enum_class: Type[_ET],
short_name: Optional[Text] = None,
case_sensitive: bool = False,
**args
):
p = _argument_parser.EnumClassParser(
enum_class, case_sensitive=case_sensitive)
g = _argument_parser.EnumClassSerializer(lowercase=not case_sensitive)
super(EnumClassFlag, self).__init__(
p, g, name, default, help, short_name, **args)
# NOTE: parser should be typed EnumClassParser[_ET] but the constructor
# restricts the available interface to ArgumentParser[_ET].
self.parser = p
self.help = '<%s>: %s' % ('|'.join(p.member_names), self.help)
def _extra_xml_dom_elements(
self, doc: minidom.Document
) -> List[minidom.Element]:
elements = []
for enum_value in self.parser.enum_class.__members__.keys():
elements.append(_helpers.create_xml_dom_element(
doc, 'enum_value', enum_value))
return elements
class MultiFlag(Generic[_T], Flag[List[_T]]):
"""A flag that can appear multiple time on the command-line.
The value of such a flag is a list that contains the individual values
from all the appearances of that flag on the command-line.
See the __doc__ for Flag for most behavior of this class. Only
differences in behavior are described here:
* The default value may be either a single value or an iterable of values.
A single value is transformed into a single-item list of that value.
* The value of the flag is always a list, even if the option was
only supplied once, and even if the default value is a single
value
"""
def __init__(self, *args, **kwargs):
super(MultiFlag, self).__init__(*args, **kwargs)
self.help += ';\n repeat this option to specify a list of values'
def parse(self, arguments: Union[Text, _T, Iterable[_T]]): # pylint: disable=arguments-renamed
"""Parses one or more arguments with the installed parser.
Args:
arguments: a single argument or a list of arguments (typically a
list of default values); a single argument is converted
internally into a list containing one item.
"""
new_values = self._parse(arguments)
if self.present:
self.value.extend(new_values)
else:
self.value = new_values
self.present += len(new_values)
def _parse(self, arguments: Union[Text, Optional[Iterable[_T]]]) -> List[_T]: # pylint: disable=arguments-renamed
if (isinstance(arguments, abc.Iterable) and
not isinstance(arguments, str)):
arguments = list(arguments)
if not isinstance(arguments, list):
# Default value may be a list of values. Most other arguments
# will not be, so convert them into a single-item list to make
# processing simpler below.
arguments = [arguments]
return [super(MultiFlag, self)._parse(item) for item in arguments]
def _serialize(self, value: Optional[List[_T]]) -> Text:
"""See base class."""
if not self.serializer:
raise _exceptions.Error(
'Serializer not present for flag %s' % self.name)
if value is None:
return ''
serialized_items = [
super(MultiFlag, self)._serialize(value_item) for value_item in value
]
return '\n'.join(serialized_items)
def flag_type(self):
"""See base class."""
return 'multi ' + self.parser.flag_type()
def _extra_xml_dom_elements(
self, doc: minidom.Document
) -> List[minidom.Element]:
elements = []
if hasattr(self.parser, 'enum_values'):
for enum_value in self.parser.enum_values: # pytype: disable=attribute-error
elements.append(_helpers.create_xml_dom_element(
doc, 'enum_value', enum_value))
return elements
class MultiEnumClassFlag(MultiFlag[_ET]): # pytype: disable=not-indexable
"""A multi_enum_class flag.
See the __doc__ for MultiFlag for most behaviors of this class. In addition,
this class knows how to handle enum.Enum instances as values for this flag
type.
"""
def __init__(
self,
name: str,
default: Union[None, Iterable[_ET], _ET, Iterable[Text], Text],
help_string: str,
enum_class: Type[_ET],
case_sensitive: bool = False,
**args
):
p = _argument_parser.EnumClassParser(
enum_class, case_sensitive=case_sensitive)
g = _argument_parser.EnumClassListSerializer(
list_sep=',', lowercase=not case_sensitive)
super(MultiEnumClassFlag, self).__init__(
p, g, name, default, help_string, **args)
# NOTE: parser should be typed EnumClassParser[_ET] but the constructor
# restricts the available interface to ArgumentParser[str].
self.parser = p
# NOTE: serializer should be non-Optional but this isn't inferred.
self.serializer = g
self.help = (
'<%s>: %s;\n repeat this option to specify a list of values' %
('|'.join(p.member_names), help_string or '(no help available)'))
def _extra_xml_dom_elements(
self, doc: minidom.Document
) -> List[minidom.Element]:
elements = []
for enum_value in self.parser.enum_class.__members__.keys(): # pytype: disable=attribute-error
elements.append(_helpers.create_xml_dom_element(
doc, 'enum_value', enum_value))
return elements
def _serialize_value_for_xml(self, value):
"""See base class."""
if value is not None:
if not self.serializer:
raise _exceptions.Error(
'Serializer not present for flag %s' % self.name
)
value_serialized = self.serializer.serialize(value)
else:
value_serialized = ''
return value_serialized

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,421 @@
# Copyright 2017 The Abseil Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Internal helper functions for Abseil Python flags library."""
import os
import re
import struct
import sys
import textwrap
import types
from typing import Any, Dict, Iterable, List, NamedTuple, Optional, Sequence, Set
from xml.dom import minidom
# pylint: disable=g-import-not-at-top
try:
import fcntl
except ImportError:
fcntl = None
try:
# Importing termios will fail on non-unix platforms.
import termios
except ImportError:
termios = None
# pylint: enable=g-import-not-at-top
_DEFAULT_HELP_WIDTH = 80 # Default width of help output.
# Minimal "sane" width of help output. We assume that any value below 40 is
# unreasonable.
_MIN_HELP_WIDTH = 40
# Define the allowed error rate in an input string to get suggestions.
#
# We lean towards a high threshold because we tend to be matching a phrase,
# and the simple algorithm used here is geared towards correcting word
# spellings.
#
# For manual testing, consider "<command> --list" which produced a large number
# of spurious suggestions when we used "least_errors > 0.5" instead of
# "least_erros >= 0.5".
_SUGGESTION_ERROR_RATE_THRESHOLD = 0.50
# Characters that cannot appear or are highly discouraged in an XML 1.0
# document. (See http://www.w3.org/TR/REC-xml/#charsets or
# https://en.wikipedia.org/wiki/Valid_characters_in_XML#XML_1.0)
_ILLEGAL_XML_CHARS_REGEX = re.compile(
u'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x84\x86-\x9f\ud800-\udfff\ufffe\uffff]')
# This is a set of module ids for the modules that disclaim key flags.
# This module is explicitly added to this set so that we never consider it to
# define key flag.
disclaim_module_ids: Set[int] = set([id(sys.modules[__name__])])
# Define special flags here so that help may be generated for them.
# NOTE: Please do NOT use SPECIAL_FLAGS from outside flags module.
# Initialized inside flagvalues.py.
# NOTE: This cannot be annotated as its actual FlagValues type since this would
# create a circular dependency.
SPECIAL_FLAGS: Any = None
# This points to the flags module, initialized in flags/__init__.py.
# This should only be used in adopt_module_key_flags to take SPECIAL_FLAGS into
# account.
FLAGS_MODULE: types.ModuleType = None
class _ModuleObjectAndName(NamedTuple):
"""Module object and name.
Fields:
- module: object, module object.
- module_name: str, module name.
"""
module: types.ModuleType
module_name: str
def get_module_object_and_name(
globals_dict: Dict[str, Any]
) -> _ModuleObjectAndName:
"""Returns the module that defines a global environment, and its name.
Args:
globals_dict: A dictionary that should correspond to an environment
providing the values of the globals.
Returns:
_ModuleObjectAndName - pair of module object & module name.
Returns (None, None) if the module could not be identified.
"""
name = globals_dict.get('__name__', None)
module = sys.modules.get(name, None)
# Pick a more informative name for the main module.
return _ModuleObjectAndName(module,
(sys.argv[0] if name == '__main__' else name))
def get_calling_module_object_and_name() -> _ModuleObjectAndName:
"""Returns the module that's calling into this module.
We generally use this function to get the name of the module calling a
DEFINE_foo... function.
Returns:
The module object that called into this one.
Raises:
AssertionError: Raised when no calling module could be identified.
"""
for depth in range(1, sys.getrecursionlimit()):
# sys._getframe is the right thing to use here, as it's the best
# way to walk up the call stack.
globals_for_frame = sys._getframe(depth).f_globals # pylint: disable=protected-access
module, module_name = get_module_object_and_name(globals_for_frame)
if id(module) not in disclaim_module_ids and module_name is not None:
return _ModuleObjectAndName(module, module_name)
raise AssertionError('No module was found')
def get_calling_module() -> str:
"""Returns the name of the module that's calling into this module."""
return get_calling_module_object_and_name().module_name
def create_xml_dom_element(
doc: minidom.Document, name: str, value: Any
) -> minidom.Element:
"""Returns an XML DOM element with name and text value.
Args:
doc: minidom.Document, the DOM document it should create nodes from.
name: str, the tag of XML element.
value: object, whose string representation will be used
as the value of the XML element. Illegal or highly discouraged xml 1.0
characters are stripped.
Returns:
An instance of minidom.Element.
"""
s = str(value)
if isinstance(value, bool):
# Display boolean values as the C++ flag library does: no caps.
s = s.lower()
# Remove illegal xml characters.
s = _ILLEGAL_XML_CHARS_REGEX.sub(u'', s)
e = doc.createElement(name)
e.appendChild(doc.createTextNode(s))
return e
def get_help_width() -> int:
"""Returns the integer width of help lines that is used in TextWrap."""
if not sys.stdout.isatty() or termios is None or fcntl is None:
return _DEFAULT_HELP_WIDTH
try:
data = fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, b'1234')
columns = struct.unpack('hh', data)[1]
# Emacs mode returns 0.
# Here we assume that any value below 40 is unreasonable.
if columns >= _MIN_HELP_WIDTH:
return columns
# Returning an int as default is fine, int(int) just return the int.
return int(os.getenv('COLUMNS', _DEFAULT_HELP_WIDTH))
except (TypeError, IOError, struct.error):
return _DEFAULT_HELP_WIDTH
def get_flag_suggestions(
attempt: Optional[str], longopt_list: Sequence[str]
) -> List[str]:
"""Returns helpful similar matches for an invalid flag."""
# Don't suggest on very short strings, or if no longopts are specified.
if len(attempt) <= 2 or not longopt_list:
return []
option_names = [v.split('=')[0] for v in longopt_list]
# Find close approximations in flag prefixes.
# This also handles the case where the flag is spelled right but ambiguous.
distances = [(_damerau_levenshtein(attempt, option[0:len(attempt)]), option)
for option in option_names]
# t[0] is distance, and sorting by t[1] allows us to have stable output.
distances.sort()
least_errors, _ = distances[0]
# Don't suggest excessively bad matches.
if least_errors >= _SUGGESTION_ERROR_RATE_THRESHOLD * len(attempt):
return []
suggestions = []
for errors, name in distances:
if errors == least_errors:
suggestions.append(name)
else:
break
return suggestions
def _damerau_levenshtein(a, b):
"""Returns Damerau-Levenshtein edit distance from a to b."""
memo = {}
def distance(x, y):
"""Recursively defined string distance with memoization."""
if (x, y) in memo:
return memo[x, y]
if not x:
d = len(y)
elif not y:
d = len(x)
else:
d = min(
distance(x[1:], y) + 1, # correct an insertion error
distance(x, y[1:]) + 1, # correct a deletion error
distance(x[1:], y[1:]) + (x[0] != y[0])) # correct a wrong character
if len(x) >= 2 and len(y) >= 2 and x[0] == y[1] and x[1] == y[0]:
# Correct a transposition.
t = distance(x[2:], y[2:]) + 1
if d > t:
d = t
memo[x, y] = d
return d
return distance(a, b)
def text_wrap(
text: str,
length: Optional[int] = None,
indent: str = '',
firstline_indent: Optional[str] = None,
) -> str:
"""Wraps a given text to a maximum line length and returns it.
It turns lines that only contain whitespace into empty lines, keeps new lines,
and expands tabs using 4 spaces.
Args:
text: str, text to wrap.
length: int, maximum length of a line, includes indentation.
If this is None then use get_help_width()
indent: str, indent for all but first line.
firstline_indent: str, indent for first line; if None, fall back to indent.
Returns:
str, the wrapped text.
Raises:
ValueError: Raised if indent or firstline_indent not shorter than length.
"""
# Get defaults where callee used None
if length is None:
length = get_help_width()
if indent is None:
indent = ''
if firstline_indent is None:
firstline_indent = indent
if len(indent) >= length:
raise ValueError('Length of indent exceeds length')
if len(firstline_indent) >= length:
raise ValueError('Length of first line indent exceeds length')
text = text.expandtabs(4)
result = []
# Create one wrapper for the first paragraph and one for subsequent
# paragraphs that does not have the initial wrapping.
wrapper = textwrap.TextWrapper(
width=length, initial_indent=firstline_indent, subsequent_indent=indent)
subsequent_wrapper = textwrap.TextWrapper(
width=length, initial_indent=indent, subsequent_indent=indent)
# textwrap does not have any special treatment for newlines. From the docs:
# "...newlines may appear in the middle of a line and cause strange output.
# For this reason, text should be split into paragraphs (using
# str.splitlines() or similar) which are wrapped separately."
for paragraph in (p.strip() for p in text.splitlines()):
if paragraph:
result.extend(wrapper.wrap(paragraph))
else:
result.append('') # Keep empty lines.
# Replace initial wrapper with wrapper for subsequent paragraphs.
wrapper = subsequent_wrapper
return '\n'.join(result)
def flag_dict_to_args(
flag_map: Dict[str, Any], multi_flags: Optional[Set[str]] = None
) -> Iterable[str]:
"""Convert a dict of values into process call parameters.
This method is used to convert a dictionary into a sequence of parameters
for a binary that parses arguments using this module.
Args:
flag_map: dict, a mapping where the keys are flag names (strings).
values are treated according to their type:
* If value is ``None``, then only the name is emitted.
* If value is ``True``, then only the name is emitted.
* If value is ``False``, then only the name prepended with 'no' is
emitted.
* If value is a string then ``--name=value`` is emitted.
* If value is a collection, this will emit
``--name=value1,value2,value3``, unless the flag name is in
``multi_flags``, in which case this will emit
``--name=value1 --name=value2 --name=value3``.
* Everything else is converted to string an passed as such.
multi_flags: set, names (strings) of flags that should be treated as
multi-flags.
Yields:
sequence of string suitable for a subprocess execution.
"""
for key, value in flag_map.items():
if value is None:
yield '--%s' % key
elif isinstance(value, bool):
if value:
yield '--%s' % key
else:
yield '--no%s' % key
elif isinstance(value, (bytes, type(u''))):
# We don't want strings to be handled like python collections.
yield '--%s=%s' % (key, value)
else:
# Now we attempt to deal with collections.
try:
if multi_flags and key in multi_flags:
for item in value:
yield '--%s=%s' % (key, str(item))
else:
yield '--%s=%s' % (key, ','.join(str(item) for item in value))
except TypeError:
# Default case.
yield '--%s=%s' % (key, value)
def trim_docstring(docstring: str) -> str:
"""Removes indentation from triple-quoted strings.
This is the function specified in PEP 257 to handle docstrings:
https://www.python.org/dev/peps/pep-0257/.
Args:
docstring: str, a python docstring.
Returns:
str, docstring with indentation removed.
"""
if not docstring:
return ''
# If you've got a line longer than this you have other problems...
max_indent = 1 << 29
# Convert tabs to spaces (following the normal Python rules)
# and split into a list of lines:
lines = docstring.expandtabs().splitlines()
# Determine minimum indentation (first line doesn't count):
indent = max_indent
for line in lines[1:]:
stripped = line.lstrip()
if stripped:
indent = min(indent, len(line) - len(stripped))
# Remove indentation (first line is special):
trimmed = [lines[0].strip()]
if indent < max_indent:
for line in lines[1:]:
trimmed.append(line[indent:].rstrip())
# Strip off trailing and leading blank lines:
while trimmed and not trimmed[-1]:
trimmed.pop()
while trimmed and not trimmed[0]:
trimmed.pop(0)
# Return a single string:
return '\n'.join(trimmed)
def doc_to_help(doc: str) -> str:
"""Takes a __doc__ string and reformats it as help."""
# Get rid of starting and ending white space. Using lstrip() or even
# strip() could drop more than maximum of first line and right space
# of last line.
doc = doc.strip()
# Get rid of all empty lines.
whitespace_only_line = re.compile('^[ \t]+$', re.M)
doc = whitespace_only_line.sub('', doc)
# Cut out common space at line beginnings.
doc = trim_docstring(doc)
# Just like this module's comment, comments tend to be aligned somehow.
# In other words they all start with the same amount of white space.
# 1) keep double new lines;
# 2) keep ws after new lines if not empty line;
# 3) all other new lines shall be changed to a space;
# Solution: Match new lines between non white space and replace with space.
doc = re.sub(r'(?<=\S)\n(?=\S)', ' ', doc, flags=re.M)
return doc

View File

@ -0,0 +1,352 @@
# Copyright 2017 The Abseil Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Module to enforce different constraints on flags.
Flags validators can be registered using following functions / decorators::
flags.register_validator
@flags.validator
flags.register_multi_flags_validator
@flags.multi_flags_validator
Three convenience functions are also provided for common flag constraints::
flags.mark_flag_as_required
flags.mark_flags_as_required
flags.mark_flags_as_mutual_exclusive
flags.mark_bool_flags_as_mutual_exclusive
See their docstring in this module for a usage manual.
Do NOT import this module directly. Import the flags package and use the
aliases defined at the package level instead.
"""
import warnings
from absl.flags import _exceptions
from absl.flags import _flagvalues
from absl.flags import _validators_classes
def register_validator(flag_name,
checker,
message='Flag validation failed',
flag_values=_flagvalues.FLAGS):
"""Adds a constraint, which will be enforced during program execution.
The constraint is validated when flags are initially parsed, and after each
change of the corresponding flag's value.
Args:
flag_name: str | FlagHolder, name or holder of the flag to be checked.
Positional-only parameter.
checker: callable, a function to validate the flag.
* input - A single positional argument: The value of the corresponding
flag (string, boolean, etc. This value will be passed to checker
by the library).
* output - bool, True if validator constraint is satisfied.
If constraint is not satisfied, it should either ``return False`` or
``raise flags.ValidationError(desired_error_message)``.
message: str, error text to be shown to the user if checker returns False.
If checker raises flags.ValidationError, message from the raised
error will be shown.
flag_values: flags.FlagValues, optional FlagValues instance to validate
against.
Raises:
AttributeError: Raised when flag_name is not registered as a valid flag
name.
ValueError: Raised when flag_values is non-default and does not match the
FlagValues of the provided FlagHolder instance.
"""
flag_name, flag_values = _flagvalues.resolve_flag_ref(flag_name, flag_values)
v = _validators_classes.SingleFlagValidator(flag_name, checker, message)
_add_validator(flag_values, v)
def validator(flag_name, message='Flag validation failed',
flag_values=_flagvalues.FLAGS):
"""A function decorator for defining a flag validator.
Registers the decorated function as a validator for flag_name, e.g.::
@flags.validator('foo')
def _CheckFoo(foo):
...
See :func:`register_validator` for the specification of checker function.
Args:
flag_name: str | FlagHolder, name or holder of the flag to be checked.
Positional-only parameter.
message: str, error text to be shown to the user if checker returns False.
If checker raises flags.ValidationError, message from the raised
error will be shown.
flag_values: flags.FlagValues, optional FlagValues instance to validate
against.
Returns:
A function decorator that registers its function argument as a validator.
Raises:
AttributeError: Raised when flag_name is not registered as a valid flag
name.
"""
def decorate(function):
register_validator(flag_name, function,
message=message,
flag_values=flag_values)
return function
return decorate
def register_multi_flags_validator(flag_names,
multi_flags_checker,
message='Flags validation failed',
flag_values=_flagvalues.FLAGS):
"""Adds a constraint to multiple flags.
The constraint is validated when flags are initially parsed, and after each
change of the corresponding flag's value.
Args:
flag_names: [str | FlagHolder], a list of the flag names or holders to be
checked. Positional-only parameter.
multi_flags_checker: callable, a function to validate the flag.
* input - dict, with keys() being flag_names, and value for each key
being the value of the corresponding flag (string, boolean, etc).
* output - bool, True if validator constraint is satisfied.
If constraint is not satisfied, it should either return False or
raise flags.ValidationError.
message: str, error text to be shown to the user if checker returns False.
If checker raises flags.ValidationError, message from the raised
error will be shown.
flag_values: flags.FlagValues, optional FlagValues instance to validate
against.
Raises:
AttributeError: Raised when a flag is not registered as a valid flag name.
ValueError: Raised when multiple FlagValues are used in the same
invocation. This can occur when FlagHolders have different `_flagvalues`
or when str-type flag_names entries are present and the `flag_values`
argument does not match that of provided FlagHolder(s).
"""
flag_names, flag_values = _flagvalues.resolve_flag_refs(
flag_names, flag_values)
v = _validators_classes.MultiFlagsValidator(
flag_names, multi_flags_checker, message)
_add_validator(flag_values, v)
def multi_flags_validator(flag_names,
message='Flag validation failed',
flag_values=_flagvalues.FLAGS):
"""A function decorator for defining a multi-flag validator.
Registers the decorated function as a validator for flag_names, e.g.::
@flags.multi_flags_validator(['foo', 'bar'])
def _CheckFooBar(flags_dict):
...
See :func:`register_multi_flags_validator` for the specification of checker
function.
Args:
flag_names: [str | FlagHolder], a list of the flag names or holders to be
checked. Positional-only parameter.
message: str, error text to be shown to the user if checker returns False.
If checker raises flags.ValidationError, message from the raised
error will be shown.
flag_values: flags.FlagValues, optional FlagValues instance to validate
against.
Returns:
A function decorator that registers its function argument as a validator.
Raises:
AttributeError: Raised when a flag is not registered as a valid flag name.
"""
def decorate(function):
register_multi_flags_validator(flag_names,
function,
message=message,
flag_values=flag_values)
return function
return decorate
def mark_flag_as_required(flag_name, flag_values=_flagvalues.FLAGS):
"""Ensures that flag is not None during program execution.
Registers a flag validator, which will follow usual validator rules.
Important note: validator will pass for any non-``None`` value, such as
``False``, ``0`` (zero), ``''`` (empty string) and so on.
If your module might be imported by others, and you only wish to make the flag
required when the module is directly executed, call this method like this::
if __name__ == '__main__':
flags.mark_flag_as_required('your_flag_name')
app.run()
Args:
flag_name: str | FlagHolder, name or holder of the flag.
Positional-only parameter.
flag_values: flags.FlagValues, optional :class:`~absl.flags.FlagValues`
instance where the flag is defined.
Raises:
AttributeError: Raised when flag_name is not registered as a valid flag
name.
ValueError: Raised when flag_values is non-default and does not match the
FlagValues of the provided FlagHolder instance.
"""
flag_name, flag_values = _flagvalues.resolve_flag_ref(flag_name, flag_values)
if flag_values[flag_name].default is not None:
warnings.warn(
'Flag --%s has a non-None default value; therefore, '
'mark_flag_as_required will pass even if flag is not specified in the '
'command line!' % flag_name,
stacklevel=2)
register_validator(
flag_name,
lambda value: value is not None,
message='Flag --{} must have a value other than None.'.format(flag_name),
flag_values=flag_values)
def mark_flags_as_required(flag_names, flag_values=_flagvalues.FLAGS):
"""Ensures that flags are not None during program execution.
If your module might be imported by others, and you only wish to make the flag
required when the module is directly executed, call this method like this::
if __name__ == '__main__':
flags.mark_flags_as_required(['flag1', 'flag2', 'flag3'])
app.run()
Args:
flag_names: Sequence[str | FlagHolder], names or holders of the flags.
flag_values: flags.FlagValues, optional FlagValues instance where the flags
are defined.
Raises:
AttributeError: If any of flag name has not already been defined as a flag.
"""
for flag_name in flag_names:
mark_flag_as_required(flag_name, flag_values)
def mark_flags_as_mutual_exclusive(flag_names, required=False,
flag_values=_flagvalues.FLAGS):
"""Ensures that only one flag among flag_names is not None.
Important note: This validator checks if flag values are ``None``, and it does
not distinguish between default and explicit values. Therefore, this validator
does not make sense when applied to flags with default values other than None,
including other false values (e.g. ``False``, ``0``, ``''``, ``[]``). That
includes multi flags with a default value of ``[]`` instead of None.
Args:
flag_names: [str | FlagHolder], names or holders of flags.
Positional-only parameter.
required: bool. If true, exactly one of the flags must have a value other
than None. Otherwise, at most one of the flags can have a value other
than None, and it is valid for all of the flags to be None.
flag_values: flags.FlagValues, optional FlagValues instance where the flags
are defined.
Raises:
ValueError: Raised when multiple FlagValues are used in the same
invocation. This can occur when FlagHolders have different `_flagvalues`
or when str-type flag_names entries are present and the `flag_values`
argument does not match that of provided FlagHolder(s).
"""
flag_names, flag_values = _flagvalues.resolve_flag_refs(
flag_names, flag_values)
for flag_name in flag_names:
if flag_values[flag_name].default is not None:
warnings.warn(
'Flag --{} has a non-None default value. That does not make sense '
'with mark_flags_as_mutual_exclusive, which checks whether the '
'listed flags have a value other than None.'.format(flag_name),
stacklevel=2)
def validate_mutual_exclusion(flags_dict):
flag_count = sum(1 for val in flags_dict.values() if val is not None)
if flag_count == 1 or (not required and flag_count == 0):
return True
raise _exceptions.ValidationError(
'{} one of ({}) must have a value other than None.'.format(
'Exactly' if required else 'At most', ', '.join(flag_names)))
register_multi_flags_validator(
flag_names, validate_mutual_exclusion, flag_values=flag_values)
def mark_bool_flags_as_mutual_exclusive(flag_names, required=False,
flag_values=_flagvalues.FLAGS):
"""Ensures that only one flag among flag_names is True.
Args:
flag_names: [str | FlagHolder], names or holders of flags.
Positional-only parameter.
required: bool. If true, exactly one flag must be True. Otherwise, at most
one flag can be True, and it is valid for all flags to be False.
flag_values: flags.FlagValues, optional FlagValues instance where the flags
are defined.
Raises:
ValueError: Raised when multiple FlagValues are used in the same
invocation. This can occur when FlagHolders have different `_flagvalues`
or when str-type flag_names entries are present and the `flag_values`
argument does not match that of provided FlagHolder(s).
"""
flag_names, flag_values = _flagvalues.resolve_flag_refs(
flag_names, flag_values)
for flag_name in flag_names:
if not flag_values[flag_name].boolean:
raise _exceptions.ValidationError(
'Flag --{} is not Boolean, which is required for flags used in '
'mark_bool_flags_as_mutual_exclusive.'.format(flag_name))
def validate_boolean_mutual_exclusion(flags_dict):
flag_count = sum(bool(val) for val in flags_dict.values())
if flag_count == 1 or (not required and flag_count == 0):
return True
raise _exceptions.ValidationError(
'{} one of ({}) must be True.'.format(
'Exactly' if required else 'At most', ', '.join(flag_names)))
register_multi_flags_validator(
flag_names, validate_boolean_mutual_exclusion, flag_values=flag_values)
def _add_validator(fv, validator_instance):
"""Register new flags validator to be checked.
Args:
fv: flags.FlagValues, the FlagValues instance to add the validator.
validator_instance: validators.Validator, the validator to add.
Raises:
KeyError: Raised when validators work with a non-existing flag.
"""
for flag_name in validator_instance.get_flags_names():
fv[flag_name].validators.append(validator_instance)

View File

@ -0,0 +1,172 @@
# Copyright 2021 The Abseil Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Defines *private* classes used for flag validators.
Do NOT import this module. DO NOT use anything from this module. They are
private APIs.
"""
from absl.flags import _exceptions
class Validator(object):
"""Base class for flags validators.
Users should NOT overload these classes, and use flags.Register...
methods instead.
"""
# Used to assign each validator an unique insertion_index
validators_count = 0
def __init__(self, checker, message):
"""Constructor to create all validators.
Args:
checker: function to verify the constraint.
Input of this method varies, see SingleFlagValidator and
multi_flags_validator for a detailed description.
message: str, error message to be shown to the user.
"""
self.checker = checker
self.message = message
Validator.validators_count += 1
# Used to assert validators in the order they were registered.
self.insertion_index = Validator.validators_count
def verify(self, flag_values):
"""Verifies that constraint is satisfied.
flags library calls this method to verify Validator's constraint.
Args:
flag_values: flags.FlagValues, the FlagValues instance to get flags from.
Raises:
Error: Raised if constraint is not satisfied.
"""
param = self._get_input_to_checker_function(flag_values)
if not self.checker(param):
raise _exceptions.ValidationError(self.message)
def get_flags_names(self):
"""Returns the names of the flags checked by this validator.
Returns:
[string], names of the flags.
"""
raise NotImplementedError('This method should be overloaded')
def print_flags_with_values(self, flag_values):
raise NotImplementedError('This method should be overloaded')
def _get_input_to_checker_function(self, flag_values):
"""Given flag values, returns the input to be given to checker.
Args:
flag_values: flags.FlagValues, containing all flags.
Returns:
The input to be given to checker. The return type depends on the specific
validator.
"""
raise NotImplementedError('This method should be overloaded')
class SingleFlagValidator(Validator):
"""Validator behind register_validator() method.
Validates that a single flag passes its checker function. The checker function
takes the flag value and returns True (if value looks fine) or, if flag value
is not valid, either returns False or raises an Exception.
"""
def __init__(self, flag_name, checker, message):
"""Constructor.
Args:
flag_name: string, name of the flag.
checker: function to verify the validator.
input - value of the corresponding flag (string, boolean, etc).
output - bool, True if validator constraint is satisfied.
If constraint is not satisfied, it should either return False or
raise flags.ValidationError(desired_error_message).
message: str, error message to be shown to the user if validator's
condition is not satisfied.
"""
super(SingleFlagValidator, self).__init__(checker, message)
self.flag_name = flag_name
def get_flags_names(self):
return [self.flag_name]
def print_flags_with_values(self, flag_values):
return 'flag --%s=%s' % (self.flag_name, flag_values[self.flag_name].value)
def _get_input_to_checker_function(self, flag_values):
"""Given flag values, returns the input to be given to checker.
Args:
flag_values: flags.FlagValues, the FlagValues instance to get flags from.
Returns:
object, the input to be given to checker.
"""
return flag_values[self.flag_name].value
class MultiFlagsValidator(Validator):
"""Validator behind register_multi_flags_validator method.
Validates that flag values pass their common checker function. The checker
function takes flag values and returns True (if values look fine) or,
if values are not valid, either returns False or raises an Exception.
"""
def __init__(self, flag_names, checker, message):
"""Constructor.
Args:
flag_names: [str], containing names of the flags used by checker.
checker: function to verify the validator.
input - dict, with keys() being flag_names, and value for each
key being the value of the corresponding flag (string, boolean,
etc).
output - bool, True if validator constraint is satisfied.
If constraint is not satisfied, it should either return False or
raise flags.ValidationError(desired_error_message).
message: str, error message to be shown to the user if validator's
condition is not satisfied
"""
super(MultiFlagsValidator, self).__init__(checker, message)
self.flag_names = flag_names
def _get_input_to_checker_function(self, flag_values):
"""Given flag values, returns the input to be given to checker.
Args:
flag_values: flags.FlagValues, the FlagValues instance to get flags from.
Returns:
dict, with keys() being self.flag_names, and value for each key
being the value of the corresponding flag (string, boolean, etc).
"""
return dict([key, flag_values[key].value] for key in self.flag_names)
def print_flags_with_values(self, flag_values):
prefix = 'flags '
flags_with_values = []
for key in self.flag_names:
flags_with_values.append('%s=%s' % (key, flag_values[key].value))
return prefix + ', '.join(flags_with_values)
def get_flags_names(self):
return self.flag_names

View File

@ -0,0 +1,388 @@
# Copyright 2018 The Abseil Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""This module provides argparse integration with absl.flags.
``argparse_flags.ArgumentParser`` is a drop-in replacement for
:class:`argparse.ArgumentParser`. It takes care of collecting and defining absl
flags in :mod:`argparse`.
Here is a simple example::
# Assume the following absl.flags is defined in another module:
#
# from absl import flags
# flags.DEFINE_string('echo', None, 'The echo message.')
#
parser = argparse_flags.ArgumentParser(
description='A demo of absl.flags and argparse integration.')
parser.add_argument('--header', help='Header message to print.')
# The parser will also accept the absl flag `--echo`.
# The `header` value is available as `args.header` just like a regular
# argparse flag. The absl flag `--echo` continues to be available via
# `absl.flags.FLAGS` if you want to access it.
args = parser.parse_args()
# Example usages:
# ./program --echo='A message.' --header='A header'
# ./program --header 'A header' --echo 'A message.'
Here is another example demonstrates subparsers::
parser = argparse_flags.ArgumentParser(description='A subcommands demo.')
parser.add_argument('--header', help='The header message to print.')
subparsers = parser.add_subparsers(help='The command to execute.')
roll_dice_parser = subparsers.add_parser(
'roll_dice', help='Roll a dice.',
# By default, absl flags can also be specified after the sub-command.
# To only allow them before sub-command, pass
# `inherited_absl_flags=None`.
inherited_absl_flags=None)
roll_dice_parser.add_argument('--num_faces', type=int, default=6)
roll_dice_parser.set_defaults(command=roll_dice)
shuffle_parser = subparsers.add_parser('shuffle', help='Shuffle inputs.')
shuffle_parser.add_argument(
'inputs', metavar='I', nargs='+', help='Inputs to shuffle.')
shuffle_parser.set_defaults(command=shuffle)
args = parser.parse_args(argv[1:])
args.command(args)
# Example usages:
# ./program --echo='A message.' roll_dice --num_faces=6
# ./program shuffle --echo='A message.' 1 2 3 4
There are several differences between :mod:`absl.flags` and
:mod:`~absl.flags.argparse_flags`:
1. Flags defined with absl.flags are parsed differently when using the
argparse parser. Notably:
1) absl.flags allows both single-dash and double-dash for any flag, and
doesn't distinguish them; argparse_flags only allows double-dash for
flag's regular name, and single-dash for flag's ``short_name``.
2) Boolean flags in absl.flags can be specified with ``--bool``,
``--nobool``, as well as ``--bool=true/false`` (though not recommended);
in argparse_flags, it only allows ``--bool``, ``--nobool``.
2. Help related flag differences:
1) absl.flags does not define help flags, absl.app does that; argparse_flags
defines help flags unless passed with ``add_help=False``.
2) absl.app supports ``--helpxml``; argparse_flags does not.
3) argparse_flags supports ``-h``; absl.app does not.
"""
import argparse
import sys
from absl import flags
_BUILT_IN_FLAGS = frozenset({
'help',
'helpshort',
'helpfull',
'helpxml',
'flagfile',
'undefok',
})
class ArgumentParser(argparse.ArgumentParser):
"""Custom ArgumentParser class to support special absl flags."""
def __init__(self, **kwargs):
"""Initializes ArgumentParser.
Args:
**kwargs: same as argparse.ArgumentParser, except:
1. It also accepts `inherited_absl_flags`: the absl flags to inherit.
The default is the global absl.flags.FLAGS instance. Pass None to
ignore absl flags.
2. The `prefix_chars` argument must be the default value '-'.
Raises:
ValueError: Raised when prefix_chars is not '-'.
"""
prefix_chars = kwargs.get('prefix_chars', '-')
if prefix_chars != '-':
raise ValueError(
'argparse_flags.ArgumentParser only supports "-" as the prefix '
'character, found "{}".'.format(prefix_chars))
# Remove inherited_absl_flags before calling super.
self._inherited_absl_flags = kwargs.pop('inherited_absl_flags', flags.FLAGS)
# Now call super to initialize argparse.ArgumentParser before calling
# add_argument in _define_absl_flags.
super(ArgumentParser, self).__init__(**kwargs)
if self.add_help:
# -h and --help are defined in super.
# Also add the --helpshort and --helpfull flags.
self.add_argument(
# Action 'help' defines a similar flag to -h/--help.
'--helpshort', action='help',
default=argparse.SUPPRESS, help=argparse.SUPPRESS)
self.add_argument(
'--helpfull', action=_HelpFullAction,
default=argparse.SUPPRESS, help='show full help message and exit')
if self._inherited_absl_flags is not None:
self.add_argument(
'--undefok', default=argparse.SUPPRESS, help=argparse.SUPPRESS)
self._define_absl_flags(self._inherited_absl_flags)
def parse_known_args(self, args=None, namespace=None):
if args is None:
args = sys.argv[1:]
if self._inherited_absl_flags is not None:
# Handle --flagfile.
# Explicitly specify force_gnu=True, since argparse behaves like
# gnu_getopt: flags can be specified after positional arguments.
args = self._inherited_absl_flags.read_flags_from_files(
args, force_gnu=True)
undefok_missing = object()
undefok = getattr(namespace, 'undefok', undefok_missing)
namespace, args = super(ArgumentParser, self).parse_known_args(
args, namespace)
# For Python <= 2.7.8: https://bugs.python.org/issue9351, a bug where
# sub-parsers don't preserve existing namespace attributes.
# Restore the undefok attribute if a sub-parser dropped it.
if undefok is not undefok_missing:
namespace.undefok = undefok
if self._inherited_absl_flags is not None:
# Handle --undefok. At this point, `args` only contains unknown flags,
# so it won't strip defined flags that are also specified with --undefok.
# For Python <= 2.7.8: https://bugs.python.org/issue9351, a bug where
# sub-parsers don't preserve existing namespace attributes. The undefok
# attribute might not exist because a subparser dropped it.
if hasattr(namespace, 'undefok'):
args = _strip_undefok_args(namespace.undefok, args)
# absl flags are not exposed in the Namespace object. See Namespace:
# https://docs.python.org/3/library/argparse.html#argparse.Namespace.
del namespace.undefok
self._inherited_absl_flags.mark_as_parsed()
try:
self._inherited_absl_flags.validate_all_flags()
except flags.IllegalFlagValueError as e:
self.error(str(e))
return namespace, args
def _define_absl_flags(self, absl_flags):
"""Defines flags from absl_flags."""
key_flags = set(absl_flags.get_key_flags_for_module(sys.argv[0]))
for name in absl_flags:
if name in _BUILT_IN_FLAGS:
# Do not inherit built-in flags.
continue
flag_instance = absl_flags[name]
# Each flags with short_name appears in FLAGS twice, so only define
# when the dictionary key is equal to the regular name.
if name == flag_instance.name:
# Suppress the flag in the help short message if it's not a main
# module's key flag.
suppress = flag_instance not in key_flags
self._define_absl_flag(flag_instance, suppress)
def _define_absl_flag(self, flag_instance, suppress):
"""Defines a flag from the flag_instance."""
flag_name = flag_instance.name
short_name = flag_instance.short_name
argument_names = ['--' + flag_name]
if short_name:
argument_names.insert(0, '-' + short_name)
if suppress:
helptext = argparse.SUPPRESS
else:
# argparse help string uses %-formatting. Escape the literal %'s.
helptext = flag_instance.help.replace('%', '%%')
if flag_instance.boolean:
# Only add the `no` form to the long name.
argument_names.append('--no' + flag_name)
self.add_argument(
*argument_names, action=_BooleanFlagAction, help=helptext,
metavar=flag_instance.name.upper(),
flag_instance=flag_instance)
else:
self.add_argument(
*argument_names, action=_FlagAction, help=helptext,
metavar=flag_instance.name.upper(),
flag_instance=flag_instance)
class _FlagAction(argparse.Action):
"""Action class for Abseil non-boolean flags."""
def __init__(
self,
option_strings,
dest,
help, # pylint: disable=redefined-builtin
metavar,
flag_instance,
default=argparse.SUPPRESS):
"""Initializes _FlagAction.
Args:
option_strings: See argparse.Action.
dest: Ignored. The flag is always defined with dest=argparse.SUPPRESS.
help: See argparse.Action.
metavar: See argparse.Action.
flag_instance: absl.flags.Flag, the absl flag instance.
default: Ignored. The flag always uses dest=argparse.SUPPRESS so it
doesn't affect the parsing result.
"""
del dest
self._flag_instance = flag_instance
super(_FlagAction, self).__init__(
option_strings=option_strings,
dest=argparse.SUPPRESS,
help=help,
metavar=metavar)
def __call__(self, parser, namespace, values, option_string=None):
"""See https://docs.python.org/3/library/argparse.html#action-classes."""
self._flag_instance.parse(values)
self._flag_instance.using_default_value = False
class _BooleanFlagAction(argparse.Action):
"""Action class for Abseil boolean flags."""
def __init__(
self,
option_strings,
dest,
help, # pylint: disable=redefined-builtin
metavar,
flag_instance,
default=argparse.SUPPRESS):
"""Initializes _BooleanFlagAction.
Args:
option_strings: See argparse.Action.
dest: Ignored. The flag is always defined with dest=argparse.SUPPRESS.
help: See argparse.Action.
metavar: See argparse.Action.
flag_instance: absl.flags.Flag, the absl flag instance.
default: Ignored. The flag always uses dest=argparse.SUPPRESS so it
doesn't affect the parsing result.
"""
del dest, default
self._flag_instance = flag_instance
flag_names = [self._flag_instance.name]
if self._flag_instance.short_name:
flag_names.append(self._flag_instance.short_name)
self._flag_names = frozenset(flag_names)
super(_BooleanFlagAction, self).__init__(
option_strings=option_strings,
dest=argparse.SUPPRESS,
nargs=0, # Does not accept values, only `--bool` or `--nobool`.
help=help,
metavar=metavar)
def __call__(self, parser, namespace, values, option_string=None):
"""See https://docs.python.org/3/library/argparse.html#action-classes."""
if not isinstance(values, list) or values:
raise ValueError('values must be an empty list.')
if option_string.startswith('--'):
option = option_string[2:]
else:
option = option_string[1:]
if option in self._flag_names:
self._flag_instance.parse('true')
else:
if not option.startswith('no') or option[2:] not in self._flag_names:
raise ValueError('invalid option_string: ' + option_string)
self._flag_instance.parse('false')
self._flag_instance.using_default_value = False
class _HelpFullAction(argparse.Action):
"""Action class for --helpfull flag."""
def __init__(self, option_strings, dest, default, help): # pylint: disable=redefined-builtin
"""Initializes _HelpFullAction.
Args:
option_strings: See argparse.Action.
dest: Ignored. The flag is always defined with dest=argparse.SUPPRESS.
default: Ignored.
help: See argparse.Action.
"""
del dest, default
super(_HelpFullAction, self).__init__(
option_strings=option_strings,
dest=argparse.SUPPRESS,
default=argparse.SUPPRESS,
nargs=0,
help=help)
def __call__(self, parser, namespace, values, option_string=None):
"""See https://docs.python.org/3/library/argparse.html#action-classes."""
# This only prints flags when help is not argparse.SUPPRESS.
# It includes user defined argparse flags, as well as main module's
# key absl flags. Other absl flags use argparse.SUPPRESS, so they aren't
# printed here.
parser.print_help()
absl_flags = parser._inherited_absl_flags # pylint: disable=protected-access
if absl_flags is not None:
modules = sorted(absl_flags.flags_by_module_dict())
main_module = sys.argv[0]
if main_module in modules:
# The main module flags are already printed in parser.print_help().
modules.remove(main_module)
print(absl_flags._get_help_for_modules( # pylint: disable=protected-access
modules, prefix='', include_special_flags=True))
parser.exit()
def _strip_undefok_args(undefok, args):
"""Returns a new list of args after removing flags in --undefok."""
if undefok:
undefok_names = set(name.strip() for name in undefok.split(','))
undefok_names |= set('no' + name for name in undefok_names)
# Remove undefok flags.
args = [arg for arg in args if not _is_undefok(arg, undefok_names)]
return args
def _is_undefok(arg, undefok_names):
"""Returns whether we can ignore arg based on a set of undefok flag names."""
if not arg.startswith('-'):
return False
if arg.startswith('--'):
arg_without_dash = arg[2:]
else:
arg_without_dash = arg[1:]
if '=' in arg_without_dash:
name, _ = arg_without_dash.split('=', 1)
else:
name = arg_without_dash
if name in undefok_names:
return True
return False