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