Initial upload: aesthetic-immich-starter
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
# 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 sys
|
||||
|
||||
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
|
||||
@@ -0,0 +1,632 @@
|
||||
# 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
|
||||
from collections.abc import Iterable, Sequence
|
||||
import csv
|
||||
import enum
|
||||
import io
|
||||
import string
|
||||
from typing import Any, Generic, TypeVar
|
||||
from xml.dom import minidom
|
||||
|
||||
from absl.flags import _helpers
|
||||
|
||||
_T = TypeVar('_T')
|
||||
_ET = TypeVar('_ET', bound=enum.Enum)
|
||||
_N = TypeVar('_N', int, float)
|
||||
|
||||
|
||||
class _ArgumentParserCache(type):
|
||||
"""Metaclass used to cache and share argument parsers among flags."""
|
||||
|
||||
_instances: dict[Any, Any] = {}
|
||||
|
||||
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: str = ''
|
||||
|
||||
def parse(self, argument: str) -> _T | None:
|
||||
"""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 # type: ignore[return-value]
|
||||
|
||||
def flag_type(self) -> str:
|
||||
"""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) -> str:
|
||||
"""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: _N | None
|
||||
upper_bound: _N | None
|
||||
|
||||
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: str | _N) -> _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: str | _N) -> _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: float | None = None,
|
||||
upper_bound: float | None = None,
|
||||
) -> None:
|
||||
super().__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: int | float | str) -> float:
|
||||
"""Returns the float value of argument."""
|
||||
if (
|
||||
(isinstance(argument, int) and not isinstance(argument, bool))
|
||||
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) -> str:
|
||||
"""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: int | None = None, upper_bound: int | None = None
|
||||
) -> None:
|
||||
super().__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: int | str) -> int:
|
||||
"""Returns the int value of argument."""
|
||||
if isinstance(argument, int) and not isinstance(argument, bool):
|
||||
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) -> str:
|
||||
"""See base class."""
|
||||
return 'int'
|
||||
|
||||
|
||||
class BooleanParser(ArgumentParser[bool]):
|
||||
"""Parser of boolean values."""
|
||||
|
||||
def parse(self, argument: str | 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) -> str:
|
||||
"""See base class."""
|
||||
return 'bool'
|
||||
|
||||
|
||||
class EnumParser(ArgumentParser[str]):
|
||||
"""Parser of a string enum value (a string value from a given set)."""
|
||||
|
||||
def __init__(
|
||||
self, enum_values: Iterable[str], 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(f'enum_values cannot be empty, found "{enum_values}"')
|
||||
if isinstance(enum_values, str):
|
||||
raise ValueError(f'enum_values cannot be a str, found "{enum_values}"')
|
||||
super().__init__()
|
||||
self.enum_values = list(enum_values)
|
||||
self.case_sensitive = case_sensitive
|
||||
|
||||
def parse(self, argument: str) -> str:
|
||||
"""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) -> str:
|
||||
"""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(f'{enum_class} is not a subclass of Enum.')
|
||||
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().__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[str]:
|
||||
"""The accepted enum names, in lowercase if not case sensitive."""
|
||||
return self._member_names
|
||||
|
||||
def parse(self, argument: _ET | str) -> _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) -> str:
|
||||
"""See base class."""
|
||||
return 'enum class'
|
||||
|
||||
|
||||
class ListSerializer(Generic[_T], ArgumentSerializer[list[_T]]):
|
||||
|
||||
def __init__(self, list_sep: str) -> None:
|
||||
self.list_sep = list_sep
|
||||
|
||||
def serialize(self, value: list[_T]) -> str:
|
||||
"""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.
|
||||
"""
|
||||
|
||||
_element_serializer: 'EnumClassSerializer'
|
||||
|
||||
def __init__(self, list_sep: str, **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().__init__(list_sep)
|
||||
self._element_serializer = EnumClassSerializer(**kwargs)
|
||||
|
||||
def serialize(self, value: _ET | list[_ET]) -> str:
|
||||
"""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[str]):
|
||||
|
||||
def serialize(self, value: list[str]) -> str:
|
||||
"""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) -> str:
|
||||
"""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: str | None = None, name: str | None = None) -> None:
|
||||
assert name
|
||||
super().__init__()
|
||||
self._token = token
|
||||
self._name = name
|
||||
self.syntactic_help = 'a %s separated list' % self._name
|
||||
|
||||
def parse(self, argument: str) -> list[str]:
|
||||
"""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) -> str:
|
||||
"""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().__init__(',', 'comma')
|
||||
|
||||
def parse(self, argument: str | list[str]) -> list[str]:
|
||||
"""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()._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().__init__(None, name)
|
||||
|
||||
def parse(self, argument: str | list[str]) -> list[str]:
|
||||
"""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()._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
@@ -0,0 +1,107 @@
|
||||
# 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().__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."""
|
||||
@@ -0,0 +1,566 @@
|
||||
# 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.abc import Iterable
|
||||
import copy
|
||||
import enum
|
||||
import functools
|
||||
from typing import Any, Generic, TypeVar
|
||||
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: _T | None
|
||||
default_as_str: str | None
|
||||
default_unparsed: _T | None | str
|
||||
|
||||
parser: _argument_parser.ArgumentParser[_T]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parser: _argument_parser.ArgumentParser[_T],
|
||||
serializer: _argument_parser.ArgumentSerializer[_T] | None,
|
||||
name: str,
|
||||
default: _T | None | str,
|
||||
help_string: str | None,
|
||||
short_name: str | None = 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 # type: ignore[annotation-type-mismatch]
|
||||
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: _T | None = None
|
||||
self.validators: list[Any] = []
|
||||
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) -> _T | None:
|
||||
return self._value
|
||||
|
||||
@value.setter
|
||||
def value(self, value: _T | None):
|
||||
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: _T | None) -> str | None:
|
||||
"""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: str | _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: str | _T) -> _T | None:
|
||||
"""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) # type: ignore[arg-type]
|
||||
except (TypeError, ValueError, OverflowError) 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) -> str:
|
||||
"""Serializes the flag."""
|
||||
return self._serialize(self.value)
|
||||
|
||||
def _serialize(self, value: _T | None) -> str:
|
||||
"""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: _T | None | str) -> 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: str | _T) -> _T | None:
|
||||
return self._parse(value)
|
||||
|
||||
def flag_type(self) -> str:
|
||||
"""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 # type: ignore[assignment]
|
||||
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: _T | None) -> 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: str,
|
||||
default: bool | None | str,
|
||||
help: str | None, # pylint: disable=redefined-builtin
|
||||
short_name: str | None = None,
|
||||
**args
|
||||
) -> None:
|
||||
p = _argument_parser.BooleanParser()
|
||||
super().__init__(p, None, name, default, help, short_name, True, **args)
|
||||
|
||||
|
||||
class EnumFlag(Flag[str]):
|
||||
"""Basic enum flag; its value can be any string from list of enum_values."""
|
||||
|
||||
parser: _argument_parser.EnumParser
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
default: str | None,
|
||||
help: str | None, # pylint: disable=redefined-builtin
|
||||
enum_values: Iterable[str],
|
||||
short_name: str | None = None,
|
||||
case_sensitive: bool = True,
|
||||
**args
|
||||
):
|
||||
p = _argument_parser.EnumParser(enum_values, case_sensitive)
|
||||
g: _argument_parser.ArgumentSerializer[str]
|
||||
g = _argument_parser.ArgumentSerializer()
|
||||
super().__init__(p, g, name, default, help, short_name, **args)
|
||||
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."""
|
||||
|
||||
parser: _argument_parser.EnumClassParser
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
default: _ET | None | str,
|
||||
help: str | None, # pylint: disable=redefined-builtin
|
||||
enum_class: type[_ET],
|
||||
short_name: str | None = None,
|
||||
case_sensitive: bool = False,
|
||||
**args
|
||||
):
|
||||
p = _argument_parser.EnumClassParser(
|
||||
enum_class, case_sensitive=case_sensitive
|
||||
)
|
||||
g: _argument_parser.EnumClassSerializer[_ET]
|
||||
g = _argument_parser.EnumClassSerializer(lowercase=not case_sensitive)
|
||||
super().__init__(p, g, name, default, help, short_name, **args)
|
||||
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().__init__(*args, **kwargs)
|
||||
self.help += ';\n repeat this option to specify a list of values'
|
||||
|
||||
def parse(self, arguments: str | _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:
|
||||
assert self.value is not None
|
||||
self.value.extend(new_values)
|
||||
else:
|
||||
self.value = new_values
|
||||
self.present += len(new_values)
|
||||
|
||||
def _parse(self, arguments: str | _T | Iterable[_T]) -> list[_T]: # pylint: disable=arguments-renamed
|
||||
arguments_list: list[str | _T]
|
||||
|
||||
if isinstance(arguments, str):
|
||||
arguments_list = [arguments]
|
||||
|
||||
elif isinstance(arguments, Iterable):
|
||||
arguments_list = list(arguments)
|
||||
|
||||
else:
|
||||
# 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_list = [arguments]
|
||||
|
||||
return [super(MultiFlag, self)._parse(item) for item in arguments_list] # type: ignore
|
||||
|
||||
def _serialize(self, value: list[_T] | None) -> str:
|
||||
"""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) # type: ignore[arg-type]
|
||||
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.
|
||||
"""
|
||||
|
||||
parser: _argument_parser.EnumClassParser[_ET] # type: ignore[assignment]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
default: None | Iterable[_ET] | _ET | Iterable[str] | str,
|
||||
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
|
||||
g = _argument_parser.EnumClassListSerializer(
|
||||
list_sep=',', lowercase=not case_sensitive)
|
||||
super().__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
@@ -0,0 +1,403 @@
|
||||
# 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."""
|
||||
|
||||
from collections.abc import Iterable, Sequence
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import textwrap
|
||||
import types
|
||||
from typing import Any, NamedTuple
|
||||
from xml.dom import minidom
|
||||
|
||||
|
||||
_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(
|
||||
'[\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] = {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 = 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 | None:
|
||||
"""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 if the module could not be identified.
|
||||
"""
|
||||
try:
|
||||
name = globals_dict['__name__']
|
||||
module = sys.modules[name]
|
||||
except KeyError:
|
||||
return 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 = get_module_object_and_name(globals_for_frame)
|
||||
if module is not None and id(module.module) not in disclaim_module_ids:
|
||||
return module
|
||||
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('', 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."""
|
||||
size = shutil.get_terminal_size(fallback=(_DEFAULT_HELP_WIDTH, 1))
|
||||
return size.columns
|
||||
|
||||
|
||||
def get_flag_suggestions(
|
||||
attempt: 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: int | None = None,
|
||||
indent: str = '',
|
||||
firstline_indent: str | None = 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: Text to wrap.
|
||||
length: Maximum length of a line, includes indentation. If this is `None`
|
||||
then use `get_help_width()`.
|
||||
indent: Indent for all but first line.
|
||||
firstline_indent: Indent for first line. If `None`, fall back to `indent`.
|
||||
|
||||
Returns:
|
||||
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: set[str] | None = 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, str)):
|
||||
# We don't want strings to be handled like python collections.
|
||||
yield '--%s=%s' % (key, value) # type: ignore[str-bytes-safe]
|
||||
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
|
||||
@@ -0,0 +1,353 @@
|
||||
# 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=f'Flag --{flag_name} must have a value other than None.',
|
||||
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)
|
||||
@@ -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:
|
||||
"""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().__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().__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 {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
|
||||
@@ -0,0 +1,390 @@
|
||||
# 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().__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().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().__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().__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().__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 = {name.strip() for name in undefok.split(',')}
|
||||
undefok_names |= {'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
|
||||
Reference in New Issue
Block a user