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