1# Copyright 2017 The Abseil Authors.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Contains Flag class - information about single command-line flag.
16
17Do NOT import this module directly. Import the flags package and use the
18aliases defined at the package level instead.
19"""
20
21from collections import abc
22import copy
23import enum
24import functools
25from typing import Any, Dict, Generic, Iterable, List, Optional, Text, Type, TypeVar, Union
26from xml.dom import minidom
27
28from absl.flags import _argument_parser
29from absl.flags import _exceptions
30from absl.flags import _helpers
31
32_T = TypeVar('_T')
33_ET = TypeVar('_ET', bound=enum.Enum)
34
35
36@functools.total_ordering
37class Flag(Generic[_T]):
38  """Information about a command-line flag.
39
40  Attributes:
41    name: the name for this flag
42    default: the default value for this flag
43    default_unparsed: the unparsed default value for this flag.
44    default_as_str: default value as repr'd string, e.g., "'true'"
45      (or None)
46    value: the most recent parsed value of this flag set by :meth:`parse`
47    help: a help string or None if no help is available
48    short_name: the single letter alias for this flag (or None)
49    boolean: if 'true', this flag does not accept arguments
50    present: true if this flag was parsed from command line flags
51    parser: an :class:`~absl.flags.ArgumentParser` object
52    serializer: an ArgumentSerializer object
53    allow_override: the flag may be redefined without raising an error,
54      and newly defined flag overrides the old one.
55    allow_override_cpp: use the flag from C++ if available the flag
56      definition is replaced by the C++ flag after init
57    allow_hide_cpp: use the Python flag despite having a C++ flag with
58      the same name (ignore the C++ flag)
59    using_default_value: the flag value has not been set by user
60    allow_overwrite: the flag may be parsed more than once without
61      raising an error, the last set value will be used
62    allow_using_method_names: whether this flag can be defined even if
63      it has a name that conflicts with a FlagValues method.
64    validators: list of the flag validators.
65
66  The only public method of a ``Flag`` object is :meth:`parse`, but it is
67  typically only called by a :class:`~absl.flags.FlagValues` object.  The
68  :meth:`parse` method is a thin wrapper around the
69  :meth:`ArgumentParser.parse()<absl.flags.ArgumentParser.parse>` method.  The
70  parsed value is saved in ``.value``, and the ``.present`` attribute is
71  updated.  If this flag was already present, an Error is raised.
72
73  :meth:`parse` is also called during ``__init__`` to parse the default value
74  and initialize the ``.value`` attribute.  This enables other python modules to
75  safely use flags even if the ``__main__`` module neglects to parse the
76  command line arguments.  The ``.present`` attribute is cleared after
77  ``__init__`` parsing.  If the default value is set to ``None``, then the
78  ``__init__`` parsing step is skipped and the ``.value`` attribute is
79  initialized to None.
80
81  Note: The default value is also presented to the user in the help
82  string, so it is important that it be a legal value for this flag.
83  """
84
85  # NOTE: pytype doesn't find defaults without this.
86  default: Optional[_T]
87  default_as_str: Optional[Text]
88  default_unparsed: Union[Optional[_T], Text]
89
90  def __init__(
91      self,
92      parser: _argument_parser.ArgumentParser[_T],
93      serializer: Optional[_argument_parser.ArgumentSerializer[_T]],
94      name: Text,
95      default: Union[Optional[_T], Text],
96      help_string: Optional[Text],
97      short_name: Optional[Text] = None,
98      boolean: bool = False,
99      allow_override: bool = False,
100      allow_override_cpp: bool = False,
101      allow_hide_cpp: bool = False,
102      allow_overwrite: bool = True,
103      allow_using_method_names: bool = False,
104  ) -> None:
105    self.name = name
106
107    if not help_string:
108      help_string = '(no help available)'
109
110    self.help = help_string
111    self.short_name = short_name
112    self.boolean = boolean
113    self.present = 0
114    self.parser = parser
115    self.serializer = serializer
116    self.allow_override = allow_override
117    self.allow_override_cpp = allow_override_cpp
118    self.allow_hide_cpp = allow_hide_cpp
119    self.allow_overwrite = allow_overwrite
120    self.allow_using_method_names = allow_using_method_names
121
122    self.using_default_value = True
123    self._value = None
124    self.validators = []
125    if self.allow_hide_cpp and self.allow_override_cpp:
126      raise _exceptions.Error(
127          "Can't have both allow_hide_cpp (means use Python flag) and "
128          'allow_override_cpp (means use C++ flag after InitGoogle)')
129
130    self._set_default(default)
131
132  @property
133  def value(self) -> Optional[_T]:
134    return self._value
135
136  @value.setter
137  def value(self, value: Optional[_T]):
138    self._value = value
139
140  def __hash__(self):
141    return hash(id(self))
142
143  def __eq__(self, other):
144    return self is other
145
146  def __lt__(self, other):
147    if isinstance(other, Flag):
148      return id(self) < id(other)
149    return NotImplemented
150
151  def __bool__(self):
152    raise TypeError('A Flag instance would always be True. '
153                    'Did you mean to test the `.value` attribute?')
154
155  def __getstate__(self):
156    raise TypeError("can't pickle Flag objects")
157
158  def __copy__(self):
159    raise TypeError('%s does not support shallow copies. '
160                    'Use copy.deepcopy instead.' % type(self).__name__)
161
162  def __deepcopy__(self, memo: Dict[int, Any]) -> 'Flag[_T]':
163    result = object.__new__(type(self))
164    result.__dict__ = copy.deepcopy(self.__dict__, memo)
165    return result
166
167  def _get_parsed_value_as_string(self, value: Optional[_T]) -> Optional[Text]:
168    """Returns parsed flag value as string."""
169    if value is None:
170      return None
171    if self.serializer:
172      return repr(self.serializer.serialize(value))
173    if self.boolean:
174      if value:
175        return repr('true')
176      else:
177        return repr('false')
178    return repr(str(value))
179
180  def parse(self, argument: Union[Text, Optional[_T]]) -> None:
181    """Parses string and sets flag value.
182
183    Args:
184      argument: str or the correct flag value type, argument to be parsed.
185    """
186    if self.present and not self.allow_overwrite:
187      raise _exceptions.IllegalFlagValueError(
188          'flag --%s=%s: already defined as %s' % (
189              self.name, argument, self.value))
190    self.value = self._parse(argument)
191    self.present += 1
192
193  def _parse(self, argument: Union[Text, _T]) -> Optional[_T]:
194    """Internal parse function.
195
196    It returns the parsed value, and does not modify class states.
197
198    Args:
199      argument: str or the correct flag value type, argument to be parsed.
200
201    Returns:
202      The parsed value.
203    """
204    try:
205      return self.parser.parse(argument)
206    except (TypeError, ValueError) as e:  # Recast as IllegalFlagValueError.
207      raise _exceptions.IllegalFlagValueError(
208          'flag --%s=%s: %s' % (self.name, argument, e))
209
210  def unparse(self) -> None:
211    self.value = self.default
212    self.using_default_value = True
213    self.present = 0
214
215  def serialize(self) -> Text:
216    """Serializes the flag."""
217    return self._serialize(self.value)
218
219  def _serialize(self, value: Optional[_T]) -> Text:
220    """Internal serialize function."""
221    if value is None:
222      return ''
223    if self.boolean:
224      if value:
225        return '--%s' % self.name
226      else:
227        return '--no%s' % self.name
228    else:
229      if not self.serializer:
230        raise _exceptions.Error(
231            'Serializer not present for flag %s' % self.name)
232      return '--%s=%s' % (self.name, self.serializer.serialize(value))
233
234  def _set_default(self, value: Union[Optional[_T], Text]) -> None:
235    """Changes the default value (and current value too) for this Flag."""
236    self.default_unparsed = value
237    if value is None:
238      self.default = None
239    else:
240      self.default = self._parse_from_default(value)
241    self.default_as_str = self._get_parsed_value_as_string(self.default)
242    if self.using_default_value:
243      self.value = self.default
244
245  # This is split out so that aliases can skip regular parsing of the default
246  # value.
247  def _parse_from_default(self, value: Union[Text, _T]) -> Optional[_T]:
248    return self._parse(value)
249
250  def flag_type(self) -> Text:
251    """Returns a str that describes the type of the flag.
252
253    NOTE: we use strings, and not the types.*Type constants because
254    our flags can have more exotic types, e.g., 'comma separated list
255    of strings', 'whitespace separated list of strings', etc.
256    """
257    return self.parser.flag_type()
258
259  def _create_xml_dom_element(
260      self, doc: minidom.Document, module_name: str, is_key: bool = False
261  ) -> minidom.Element:
262    """Returns an XML element that contains this flag's information.
263
264    This is information that is relevant to all flags (e.g., name,
265    meaning, etc.).  If you defined a flag that has some other pieces of
266    info, then please override _ExtraXMLInfo.
267
268    Please do NOT override this method.
269
270    Args:
271      doc: minidom.Document, the DOM document it should create nodes from.
272      module_name: str,, the name of the module that defines this flag.
273      is_key: boolean, True iff this flag is key for main module.
274
275    Returns:
276      A minidom.Element instance.
277    """
278    element = doc.createElement('flag')
279    if is_key:
280      element.appendChild(_helpers.create_xml_dom_element(doc, 'key', 'yes'))
281    element.appendChild(_helpers.create_xml_dom_element(
282        doc, 'file', module_name))
283    # Adds flag features that are relevant for all flags.
284    element.appendChild(_helpers.create_xml_dom_element(doc, 'name', self.name))
285    if self.short_name:
286      element.appendChild(_helpers.create_xml_dom_element(
287          doc, 'short_name', self.short_name))
288    if self.help:
289      element.appendChild(_helpers.create_xml_dom_element(
290          doc, 'meaning', self.help))
291    # The default flag value can either be represented as a string like on the
292    # command line, or as a Python object.  We serialize this value in the
293    # latter case in order to remain consistent.
294    if self.serializer and not isinstance(self.default, str):
295      if self.default is not None:
296        default_serialized = self.serializer.serialize(self.default)
297      else:
298        default_serialized = ''
299    else:
300      default_serialized = self.default
301    element.appendChild(_helpers.create_xml_dom_element(
302        doc, 'default', default_serialized))
303    value_serialized = self._serialize_value_for_xml(self.value)
304    element.appendChild(_helpers.create_xml_dom_element(
305        doc, 'current', value_serialized))
306    element.appendChild(_helpers.create_xml_dom_element(
307        doc, 'type', self.flag_type()))
308    # Adds extra flag features this flag may have.
309    for e in self._extra_xml_dom_elements(doc):
310      element.appendChild(e)
311    return element
312
313  def _serialize_value_for_xml(self, value: Optional[_T]) -> Any:
314    """Returns the serialized value, for use in an XML help text."""
315    return value
316
317  def _extra_xml_dom_elements(
318      self, doc: minidom.Document
319  ) -> List[minidom.Element]:
320    """Returns extra info about this flag in XML.
321
322    "Extra" means "not already included by _create_xml_dom_element above."
323
324    Args:
325      doc: minidom.Document, the DOM document it should create nodes from.
326
327    Returns:
328      A list of minidom.Element.
329    """
330    # Usually, the parser knows the extra details about the flag, so
331    # we just forward the call to it.
332    return self.parser._custom_xml_dom_elements(doc)  # pylint: disable=protected-access
333
334
335class BooleanFlag(Flag[bool]):
336  """Basic boolean flag.
337
338  Boolean flags do not take any arguments, and their value is either
339  ``True`` (1) or ``False`` (0).  The false value is specified on the command
340  line by prepending the word ``'no'`` to either the long or the short flag
341  name.
342
343  For example, if a Boolean flag was created whose long name was
344  ``'update'`` and whose short name was ``'x'``, then this flag could be
345  explicitly unset through either ``--noupdate`` or ``--nox``.
346  """
347
348  def __init__(
349      self,
350      name: Text,
351      default: Union[Optional[bool], Text],
352      help: Optional[Text],  # pylint: disable=redefined-builtin
353      short_name: Optional[Text] = None,
354      **args
355  ) -> None:
356    p = _argument_parser.BooleanParser()
357    super(BooleanFlag, self).__init__(
358        p, None, name, default, help, short_name, True, **args
359    )
360
361
362class EnumFlag(Flag[Text]):
363  """Basic enum flag; its value can be any string from list of enum_values."""
364
365  def __init__(
366      self,
367      name: Text,
368      default: Optional[Text],
369      help: Optional[Text],  # pylint: disable=redefined-builtin
370      enum_values: Iterable[Text],
371      short_name: Optional[Text] = None,
372      case_sensitive: bool = True,
373      **args
374  ):
375    p = _argument_parser.EnumParser(enum_values, case_sensitive)
376    g = _argument_parser.ArgumentSerializer()
377    super(EnumFlag, self).__init__(
378        p, g, name, default, help, short_name, **args)
379    # NOTE: parser should be typed EnumParser but the constructor
380    # restricts the available interface to ArgumentParser[str].
381    self.parser = p
382    self.help = '<%s>: %s' % ('|'.join(p.enum_values), self.help)
383
384  def _extra_xml_dom_elements(
385      self, doc: minidom.Document
386  ) -> List[minidom.Element]:
387    elements = []
388    for enum_value in self.parser.enum_values:
389      elements.append(_helpers.create_xml_dom_element(
390          doc, 'enum_value', enum_value))
391    return elements
392
393
394class EnumClassFlag(Flag[_ET]):
395  """Basic enum flag; its value is an enum class's member."""
396
397  def __init__(
398      self,
399      name: Text,
400      default: Union[Optional[_ET], Text],
401      help: Optional[Text],  # pylint: disable=redefined-builtin
402      enum_class: Type[_ET],
403      short_name: Optional[Text] = None,
404      case_sensitive: bool = False,
405      **args
406  ):
407    p = _argument_parser.EnumClassParser(
408        enum_class, case_sensitive=case_sensitive)
409    g = _argument_parser.EnumClassSerializer(lowercase=not case_sensitive)
410    super(EnumClassFlag, self).__init__(
411        p, g, name, default, help, short_name, **args)
412    # NOTE: parser should be typed EnumClassParser[_ET] but the constructor
413    # restricts the available interface to ArgumentParser[_ET].
414    self.parser = p
415    self.help = '<%s>: %s' % ('|'.join(p.member_names), self.help)
416
417  def _extra_xml_dom_elements(
418      self, doc: minidom.Document
419  ) -> List[minidom.Element]:
420    elements = []
421    for enum_value in self.parser.enum_class.__members__.keys():
422      elements.append(_helpers.create_xml_dom_element(
423          doc, 'enum_value', enum_value))
424    return elements
425
426
427class MultiFlag(Generic[_T], Flag[List[_T]]):
428  """A flag that can appear multiple time on the command-line.
429
430  The value of such a flag is a list that contains the individual values
431  from all the appearances of that flag on the command-line.
432
433  See the __doc__ for Flag for most behavior of this class.  Only
434  differences in behavior are described here:
435
436    * The default value may be either a single value or an iterable of values.
437      A single value is transformed into a single-item list of that value.
438
439    * The value of the flag is always a list, even if the option was
440      only supplied once, and even if the default value is a single
441      value
442  """
443
444  def __init__(self, *args, **kwargs):
445    super(MultiFlag, self).__init__(*args, **kwargs)
446    self.help += ';\n    repeat this option to specify a list of values'
447
448  def parse(self, arguments: Union[Text, _T, Iterable[_T]]):  # pylint: disable=arguments-renamed
449    """Parses one or more arguments with the installed parser.
450
451    Args:
452      arguments: a single argument or a list of arguments (typically a
453        list of default values); a single argument is converted
454        internally into a list containing one item.
455    """
456    new_values = self._parse(arguments)
457    if self.present:
458      self.value.extend(new_values)
459    else:
460      self.value = new_values
461    self.present += len(new_values)
462
463  def _parse(self, arguments: Union[Text, Optional[Iterable[_T]]]) -> List[_T]:  # pylint: disable=arguments-renamed
464    if (isinstance(arguments, abc.Iterable) and
465        not isinstance(arguments, str)):
466      arguments = list(arguments)
467
468    if not isinstance(arguments, list):
469      # Default value may be a list of values.  Most other arguments
470      # will not be, so convert them into a single-item list to make
471      # processing simpler below.
472      arguments = [arguments]
473
474    return [super(MultiFlag, self)._parse(item) for item in arguments]
475
476  def _serialize(self, value: Optional[List[_T]]) -> Text:
477    """See base class."""
478    if not self.serializer:
479      raise _exceptions.Error(
480          'Serializer not present for flag %s' % self.name)
481    if value is None:
482      return ''
483
484    serialized_items = [
485        super(MultiFlag, self)._serialize(value_item) for value_item in value
486    ]
487
488    return '\n'.join(serialized_items)
489
490  def flag_type(self):
491    """See base class."""
492    return 'multi ' + self.parser.flag_type()
493
494  def _extra_xml_dom_elements(
495      self, doc: minidom.Document
496  ) -> List[minidom.Element]:
497    elements = []
498    if hasattr(self.parser, 'enum_values'):
499      for enum_value in self.parser.enum_values:  # pytype: disable=attribute-error
500        elements.append(_helpers.create_xml_dom_element(
501            doc, 'enum_value', enum_value))
502    return elements
503
504
505class MultiEnumClassFlag(MultiFlag[_ET]):  # pytype: disable=not-indexable
506  """A multi_enum_class flag.
507
508  See the __doc__ for MultiFlag for most behaviors of this class.  In addition,
509  this class knows how to handle enum.Enum instances as values for this flag
510  type.
511  """
512
513  def __init__(
514      self,
515      name: str,
516      default: Union[None, Iterable[_ET], _ET, Iterable[Text], Text],
517      help_string: str,
518      enum_class: Type[_ET],
519      case_sensitive: bool = False,
520      **args
521  ):
522    p = _argument_parser.EnumClassParser(
523        enum_class, case_sensitive=case_sensitive)
524    g = _argument_parser.EnumClassListSerializer(
525        list_sep=',', lowercase=not case_sensitive)
526    super(MultiEnumClassFlag, self).__init__(
527        p, g, name, default, help_string, **args)
528    # NOTE: parser should be typed EnumClassParser[_ET] but the constructor
529    # restricts the available interface to ArgumentParser[str].
530    self.parser = p
531    # NOTE: serializer should be non-Optional but this isn't inferred.
532    self.serializer = g
533    self.help = (
534        '<%s>: %s;\n    repeat this option to specify a list of values' %
535        ('|'.join(p.member_names), help_string or '(no help available)'))
536
537  def _extra_xml_dom_elements(
538      self, doc: minidom.Document
539  ) -> List[minidom.Element]:
540    elements = []
541    for enum_value in self.parser.enum_class.__members__.keys():  # pytype: disable=attribute-error
542      elements.append(_helpers.create_xml_dom_element(
543          doc, 'enum_value', enum_value))
544    return elements
545
546  def _serialize_value_for_xml(self, value):
547    """See base class."""
548    if value is not None:
549      if not self.serializer:
550        raise _exceptions.Error(
551            'Serializer not present for flag %s' % self.name
552        )
553      value_serialized = self.serializer.serialize(value)
554    else:
555      value_serialized = ''
556    return value_serialized
557