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, 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[str]
88 default_unparsed: Union[Optional[_T], str]
89
90 parser: _argument_parser.ArgumentParser[_T]
91
92 def __init__(
93 self,
94 parser: _argument_parser.ArgumentParser[_T],
95 serializer: Optional[_argument_parser.ArgumentSerializer[_T]],
96 name: str,
97 default: Union[Optional[_T], str],
98 help_string: Optional[str],
99 short_name: Optional[str] = None,
100 boolean: bool = False,
101 allow_override: bool = False,
102 allow_override_cpp: bool = False,
103 allow_hide_cpp: bool = False,
104 allow_overwrite: bool = True,
105 allow_using_method_names: bool = False,
106 ) -> None:
107 self.name = name
108
109 if not help_string:
110 help_string = '(no help available)'
111
112 self.help = help_string
113 self.short_name = short_name
114 self.boolean = boolean
115 self.present = 0
116 self.parser = parser # type: ignore[annotation-type-mismatch]
117 self.serializer = serializer
118 self.allow_override = allow_override
119 self.allow_override_cpp = allow_override_cpp
120 self.allow_hide_cpp = allow_hide_cpp
121 self.allow_overwrite = allow_overwrite
122 self.allow_using_method_names = allow_using_method_names
123
124 self.using_default_value = True
125 self._value: Optional[_T] = None
126 self.validators: List[Any] = []
127 if self.allow_hide_cpp and self.allow_override_cpp:
128 raise _exceptions.Error(
129 "Can't have both allow_hide_cpp (means use Python flag) and "
130 'allow_override_cpp (means use C++ flag after InitGoogle)')
131
132 self._set_default(default)
133
134 @property
135 def value(self) -> Optional[_T]:
136 return self._value
137
138 @value.setter
139 def value(self, value: Optional[_T]):
140 self._value = value
141
142 def __hash__(self):
143 return hash(id(self))
144
145 def __eq__(self, other):
146 return self is other
147
148 def __lt__(self, other):
149 if isinstance(other, Flag):
150 return id(self) < id(other)
151 return NotImplemented
152
153 def __bool__(self):
154 raise TypeError('A Flag instance would always be True. '
155 'Did you mean to test the `.value` attribute?')
156
157 def __getstate__(self):
158 raise TypeError("can't pickle Flag objects")
159
160 def __copy__(self):
161 raise TypeError('%s does not support shallow copies. '
162 'Use copy.deepcopy instead.' % type(self).__name__)
163
164 def __deepcopy__(self, memo: Dict[int, Any]) -> 'Flag[_T]':
165 result = object.__new__(type(self))
166 result.__dict__ = copy.deepcopy(self.__dict__, memo)
167 return result
168
169 def _get_parsed_value_as_string(self, value: Optional[_T]) -> Optional[str]:
170 """Returns parsed flag value as string."""
171 if value is None:
172 return None
173 if self.serializer:
174 return repr(self.serializer.serialize(value))
175 if self.boolean:
176 if value:
177 return repr('true')
178 else:
179 return repr('false')
180 return repr(str(value))
181
182 def parse(self, argument: Union[str, _T]) -> None:
183 """Parses string and sets flag value.
184
185 Args:
186 argument: str or the correct flag value type, argument to be parsed.
187 """
188 if self.present and not self.allow_overwrite:
189 raise _exceptions.IllegalFlagValueError(
190 'flag --%s=%s: already defined as %s' % (
191 self.name, argument, self.value))
192 self.value = self._parse(argument)
193 self.present += 1
194
195 def _parse(self, argument: Union[str, _T]) -> Optional[_T]:
196 """Internal parse function.
197
198 It returns the parsed value, and does not modify class states.
199
200 Args:
201 argument: str or the correct flag value type, argument to be parsed.
202
203 Returns:
204 The parsed value.
205 """
206 try:
207 return self.parser.parse(argument) # type: ignore[arg-type]
208 except (TypeError, ValueError, OverflowError) as e:
209 # Recast as IllegalFlagValueError.
210 raise _exceptions.IllegalFlagValueError(
211 'flag --%s=%s: %s' % (self.name, argument, e))
212
213 def unparse(self) -> None:
214 self.value = self.default
215 self.using_default_value = True
216 self.present = 0
217
218 def serialize(self) -> str:
219 """Serializes the flag."""
220 return self._serialize(self.value)
221
222 def _serialize(self, value: Optional[_T]) -> str:
223 """Internal serialize function."""
224 if value is None:
225 return ''
226 if self.boolean:
227 if value:
228 return '--%s' % self.name
229 else:
230 return '--no%s' % self.name
231 else:
232 if not self.serializer:
233 raise _exceptions.Error(
234 'Serializer not present for flag %s' % self.name)
235 return '--%s=%s' % (self.name, self.serializer.serialize(value))
236
237 def _set_default(self, value: Union[Optional[_T], str]) -> None:
238 """Changes the default value (and current value too) for this Flag."""
239 self.default_unparsed = value
240 if value is None:
241 self.default = None
242 else:
243 self.default = self._parse_from_default(value)
244 self.default_as_str = self._get_parsed_value_as_string(self.default)
245 if self.using_default_value:
246 self.value = self.default
247
248 # This is split out so that aliases can skip regular parsing of the default
249 # value.
250 def _parse_from_default(self, value: Union[str, _T]) -> Optional[_T]:
251 return self._parse(value)
252
253 def flag_type(self) -> str:
254 """Returns a str that describes the type of the flag.
255
256 NOTE: we use strings, and not the types.*Type constants because
257 our flags can have more exotic types, e.g., 'comma separated list
258 of strings', 'whitespace separated list of strings', etc.
259 """
260 return self.parser.flag_type()
261
262 def _create_xml_dom_element(
263 self, doc: minidom.Document, module_name: str, is_key: bool = False
264 ) -> minidom.Element:
265 """Returns an XML element that contains this flag's information.
266
267 This is information that is relevant to all flags (e.g., name,
268 meaning, etc.). If you defined a flag that has some other pieces of
269 info, then please override _ExtraXMLInfo.
270
271 Please do NOT override this method.
272
273 Args:
274 doc: minidom.Document, the DOM document it should create nodes from.
275 module_name: str,, the name of the module that defines this flag.
276 is_key: boolean, True iff this flag is key for main module.
277
278 Returns:
279 A minidom.Element instance.
280 """
281 element = doc.createElement('flag')
282 if is_key:
283 element.appendChild(_helpers.create_xml_dom_element(doc, 'key', 'yes'))
284 element.appendChild(_helpers.create_xml_dom_element(
285 doc, 'file', module_name))
286 # Adds flag features that are relevant for all flags.
287 element.appendChild(_helpers.create_xml_dom_element(doc, 'name', self.name))
288 if self.short_name:
289 element.appendChild(_helpers.create_xml_dom_element(
290 doc, 'short_name', self.short_name))
291 if self.help:
292 element.appendChild(_helpers.create_xml_dom_element(
293 doc, 'meaning', self.help))
294 # The default flag value can either be represented as a string like on the
295 # command line, or as a Python object. We serialize this value in the
296 # latter case in order to remain consistent.
297 if self.serializer and not isinstance(self.default, str):
298 if self.default is not None:
299 default_serialized = self.serializer.serialize(self.default)
300 else:
301 default_serialized = ''
302 else:
303 default_serialized = self.default # type: ignore[assignment]
304 element.appendChild(_helpers.create_xml_dom_element(
305 doc, 'default', default_serialized))
306 value_serialized = self._serialize_value_for_xml(self.value)
307 element.appendChild(_helpers.create_xml_dom_element(
308 doc, 'current', value_serialized))
309 element.appendChild(_helpers.create_xml_dom_element(
310 doc, 'type', self.flag_type()))
311 # Adds extra flag features this flag may have.
312 for e in self._extra_xml_dom_elements(doc):
313 element.appendChild(e)
314 return element
315
316 def _serialize_value_for_xml(self, value: Optional[_T]) -> Any:
317 """Returns the serialized value, for use in an XML help text."""
318 return value
319
320 def _extra_xml_dom_elements(
321 self, doc: minidom.Document
322 ) -> List[minidom.Element]:
323 """Returns extra info about this flag in XML.
324
325 "Extra" means "not already included by _create_xml_dom_element above."
326
327 Args:
328 doc: minidom.Document, the DOM document it should create nodes from.
329
330 Returns:
331 A list of minidom.Element.
332 """
333 # Usually, the parser knows the extra details about the flag, so
334 # we just forward the call to it.
335 return self.parser._custom_xml_dom_elements(doc) # pylint: disable=protected-access
336
337
338class BooleanFlag(Flag[bool]):
339 """Basic boolean flag.
340
341 Boolean flags do not take any arguments, and their value is either
342 ``True`` (1) or ``False`` (0). The false value is specified on the command
343 line by prepending the word ``'no'`` to either the long or the short flag
344 name.
345
346 For example, if a Boolean flag was created whose long name was
347 ``'update'`` and whose short name was ``'x'``, then this flag could be
348 explicitly unset through either ``--noupdate`` or ``--nox``.
349 """
350
351 def __init__(
352 self,
353 name: str,
354 default: Union[Optional[bool], str],
355 help: Optional[str], # pylint: disable=redefined-builtin
356 short_name: Optional[str] = None,
357 **args
358 ) -> None:
359 p = _argument_parser.BooleanParser()
360 super().__init__(p, None, name, default, help, short_name, True, **args)
361
362
363class EnumFlag(Flag[str]):
364 """Basic enum flag; its value can be any string from list of enum_values."""
365
366 parser: _argument_parser.EnumParser
367
368 def __init__(
369 self,
370 name: str,
371 default: Optional[str],
372 help: Optional[str], # pylint: disable=redefined-builtin
373 enum_values: Iterable[str],
374 short_name: Optional[str] = None,
375 case_sensitive: bool = True,
376 **args
377 ):
378 p = _argument_parser.EnumParser(enum_values, case_sensitive)
379 g: _argument_parser.ArgumentSerializer[str]
380 g = _argument_parser.ArgumentSerializer()
381 super().__init__(p, g, name, default, help, short_name, **args)
382 self.parser = p
383 self.help = '<%s>: %s' % ('|'.join(p.enum_values), self.help)
384
385 def _extra_xml_dom_elements(
386 self, doc: minidom.Document
387 ) -> List[minidom.Element]:
388 elements = []
389 for enum_value in self.parser.enum_values:
390 elements.append(_helpers.create_xml_dom_element(
391 doc, 'enum_value', enum_value))
392 return elements
393
394
395class EnumClassFlag(Flag[_ET]):
396 """Basic enum flag; its value is an enum class's member."""
397
398 parser: _argument_parser.EnumClassParser
399
400 def __init__(
401 self,
402 name: str,
403 default: Union[Optional[_ET], str],
404 help: Optional[str], # pylint: disable=redefined-builtin
405 enum_class: Type[_ET],
406 short_name: Optional[str] = None,
407 case_sensitive: bool = False,
408 **args
409 ):
410 p = _argument_parser.EnumClassParser(
411 enum_class, case_sensitive=case_sensitive
412 )
413 g: _argument_parser.EnumClassSerializer[_ET]
414 g = _argument_parser.EnumClassSerializer(lowercase=not case_sensitive)
415 super().__init__(p, g, name, default, help, short_name, **args)
416 self.parser = p
417 self.help = '<%s>: %s' % ('|'.join(p.member_names), self.help)
418
419 def _extra_xml_dom_elements(
420 self, doc: minidom.Document
421 ) -> List[minidom.Element]:
422 elements = []
423 for enum_value in self.parser.enum_class.__members__.keys():
424 elements.append(_helpers.create_xml_dom_element(
425 doc, 'enum_value', enum_value))
426 return elements
427
428
429class MultiFlag(Generic[_T], Flag[List[_T]]):
430 """A flag that can appear multiple time on the command-line.
431
432 The value of such a flag is a list that contains the individual values
433 from all the appearances of that flag on the command-line.
434
435 See the __doc__ for Flag for most behavior of this class. Only
436 differences in behavior are described here:
437
438 * The default value may be either a single value or an iterable of values.
439 A single value is transformed into a single-item list of that value.
440
441 * The value of the flag is always a list, even if the option was
442 only supplied once, and even if the default value is a single
443 value
444 """
445
446 def __init__(self, *args, **kwargs):
447 super().__init__(*args, **kwargs)
448 self.help += ';\n repeat this option to specify a list of values'
449
450 def parse(self, arguments: Union[str, _T, Iterable[_T]]): # pylint: disable=arguments-renamed
451 """Parses one or more arguments with the installed parser.
452
453 Args:
454 arguments: a single argument or a list of arguments (typically a
455 list of default values); a single argument is converted
456 internally into a list containing one item.
457 """
458 new_values = self._parse(arguments)
459 if self.present:
460 assert self.value is not None
461 self.value.extend(new_values)
462 else:
463 self.value = new_values
464 self.present += len(new_values)
465
466 def _parse(self, arguments: Union[str, _T, Iterable[_T]]) -> List[_T]: # pylint: disable=arguments-renamed
467 arguments_list: List[Union[str, _T]]
468
469 if isinstance(arguments, str):
470 arguments_list = [arguments]
471
472 elif isinstance(arguments, abc.Iterable):
473 arguments_list = list(arguments)
474
475 else:
476 # Default value may be a list of values. Most other arguments
477 # will not be, so convert them into a single-item list to make
478 # processing simpler below.
479 arguments_list = [arguments]
480
481 return [super(MultiFlag, self)._parse(item) for item in arguments_list] # type: ignore
482
483 def _serialize(self, value: Optional[List[_T]]) -> str:
484 """See base class."""
485 if not self.serializer:
486 raise _exceptions.Error(
487 'Serializer not present for flag %s' % self.name)
488 if value is None:
489 return ''
490
491 serialized_items = [
492 super(MultiFlag, self)._serialize(value_item) # type: ignore[arg-type]
493 for value_item in value
494 ]
495
496 return '\n'.join(serialized_items)
497
498 def flag_type(self):
499 """See base class."""
500 return 'multi ' + self.parser.flag_type()
501
502 def _extra_xml_dom_elements(
503 self, doc: minidom.Document
504 ) -> List[minidom.Element]:
505 elements = []
506 if hasattr(self.parser, 'enum_values'):
507 for enum_value in self.parser.enum_values: # pytype: disable=attribute-error
508 elements.append(_helpers.create_xml_dom_element(
509 doc, 'enum_value', enum_value))
510 return elements
511
512
513class MultiEnumClassFlag(MultiFlag[_ET]): # pytype: disable=not-indexable
514 """A multi_enum_class flag.
515
516 See the __doc__ for MultiFlag for most behaviors of this class. In addition,
517 this class knows how to handle enum.Enum instances as values for this flag
518 type.
519 """
520
521 parser: _argument_parser.EnumClassParser[_ET] # type: ignore[assignment]
522
523 def __init__(
524 self,
525 name: str,
526 default: Union[None, Iterable[_ET], _ET, Iterable[str], str],
527 help_string: str,
528 enum_class: Type[_ET],
529 case_sensitive: bool = False,
530 **args
531 ):
532 p = _argument_parser.EnumClassParser(
533 enum_class, case_sensitive=case_sensitive)
534 g: _argument_parser.EnumClassListSerializer
535 g = _argument_parser.EnumClassListSerializer(
536 list_sep=',', lowercase=not case_sensitive)
537 super().__init__(p, g, name, default, help_string, **args)
538 # NOTE: parser should be typed EnumClassParser[_ET] but the constructor
539 # restricts the available interface to ArgumentParser[str].
540 self.parser = p
541 # NOTE: serializer should be non-Optional but this isn't inferred.
542 self.serializer = g
543 self.help = (
544 '<%s>: %s;\n repeat this option to specify a list of values' %
545 ('|'.join(p.member_names), help_string or '(no help available)'))
546
547 def _extra_xml_dom_elements(
548 self, doc: minidom.Document
549 ) -> List[minidom.Element]:
550 elements = []
551 for enum_value in self.parser.enum_class.__members__.keys(): # pytype: disable=attribute-error
552 elements.append(_helpers.create_xml_dom_element(
553 doc, 'enum_value', enum_value))
554 return elements
555
556 def _serialize_value_for_xml(self, value):
557 """See base class."""
558 if value is not None:
559 if not self.serializer:
560 raise _exceptions.Error(
561 'Serializer not present for flag %s' % self.name
562 )
563 value_serialized = self.serializer.serialize(value)
564 else:
565 value_serialized = ''
566 return value_serialized