Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/absl_py-2.0.0-py3.8.egg/absl/flags/_argument_parser.py: 53%
269 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:13 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:13 +0000
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.
15"""Contains base classes used to parse and convert arguments.
17Do NOT import this module directly. Import the flags package and use the
18aliases defined at the package level instead.
19"""
21import collections
22import csv
23import enum
24import io
25import string
26from typing import Generic, List, Iterable, Optional, Sequence, Text, Type, TypeVar, Union
27from xml.dom import minidom
29from absl.flags import _helpers
31_T = TypeVar('_T')
32_ET = TypeVar('_ET', bound=enum.Enum)
33_N = TypeVar('_N', int, float)
36def _is_integer_type(instance):
37 """Returns True if instance is an integer, and not a bool."""
38 return (isinstance(instance, int) and
39 not isinstance(instance, bool))
42class _ArgumentParserCache(type):
43 """Metaclass used to cache and share argument parsers among flags."""
45 _instances = {}
47 def __call__(cls, *args, **kwargs):
48 """Returns an instance of the argument parser cls.
50 This method overrides behavior of the __new__ methods in
51 all subclasses of ArgumentParser (inclusive). If an instance
52 for cls with the same set of arguments exists, this instance is
53 returned, otherwise a new instance is created.
55 If any keyword arguments are defined, or the values in args
56 are not hashable, this method always returns a new instance of
57 cls.
59 Args:
60 *args: Positional initializer arguments.
61 **kwargs: Initializer keyword arguments.
63 Returns:
64 An instance of cls, shared or new.
65 """
66 if kwargs:
67 return type.__call__(cls, *args, **kwargs)
68 else:
69 instances = cls._instances
70 key = (cls,) + tuple(args)
71 try:
72 return instances[key]
73 except KeyError:
74 # No cache entry for key exists, create a new one.
75 return instances.setdefault(key, type.__call__(cls, *args))
76 except TypeError:
77 # An object in args cannot be hashed, always return
78 # a new instance.
79 return type.__call__(cls, *args)
82class ArgumentParser(Generic[_T], metaclass=_ArgumentParserCache):
83 """Base class used to parse and convert arguments.
85 The :meth:`parse` method checks to make sure that the string argument is a
86 legal value and convert it to a native type. If the value cannot be
87 converted, it should throw a ``ValueError`` exception with a human
88 readable explanation of why the value is illegal.
90 Subclasses should also define a syntactic_help string which may be
91 presented to the user to describe the form of the legal values.
93 Argument parser classes must be stateless, since instances are cached
94 and shared between flags. Initializer arguments are allowed, but all
95 member variables must be derived from initializer arguments only.
96 """
98 syntactic_help: Text = ''
100 def parse(self, argument: Text) -> Optional[_T]:
101 """Parses the string argument and returns the native value.
103 By default it returns its argument unmodified.
105 Args:
106 argument: string argument passed in the commandline.
108 Raises:
109 ValueError: Raised when it fails to parse the argument.
110 TypeError: Raised when the argument has the wrong type.
112 Returns:
113 The parsed value in native type.
114 """
115 if not isinstance(argument, str):
116 raise TypeError('flag value must be a string, found "{}"'.format(
117 type(argument)))
118 return argument
120 def flag_type(self) -> Text:
121 """Returns a string representing the type of the flag."""
122 return 'string'
124 def _custom_xml_dom_elements(
125 self, doc: minidom.Document
126 ) -> List[minidom.Element]:
127 """Returns a list of minidom.Element to add additional flag information.
129 Args:
130 doc: minidom.Document, the DOM document it should create nodes from.
131 """
132 del doc # Unused.
133 return []
136class ArgumentSerializer(Generic[_T]):
137 """Base class for generating string representations of a flag value."""
139 def serialize(self, value: _T) -> Text:
140 """Returns a serialized string of the value."""
141 return str(value)
144class NumericParser(ArgumentParser[_N]):
145 """Parser of numeric values.
147 Parsed value may be bounded to a given upper and lower bound.
148 """
150 lower_bound: Optional[_N]
151 upper_bound: Optional[_N]
153 def is_outside_bounds(self, val: _N) -> bool:
154 """Returns whether the value is outside the bounds or not."""
155 return ((self.lower_bound is not None and val < self.lower_bound) or
156 (self.upper_bound is not None and val > self.upper_bound))
158 def parse(self, argument: Text) -> _N:
159 """See base class."""
160 val = self.convert(argument)
161 if self.is_outside_bounds(val):
162 raise ValueError('%s is not %s' % (val, self.syntactic_help))
163 return val
165 def _custom_xml_dom_elements(
166 self, doc: minidom.Document
167 ) -> List[minidom.Element]:
168 elements = []
169 if self.lower_bound is not None:
170 elements.append(_helpers.create_xml_dom_element(
171 doc, 'lower_bound', self.lower_bound))
172 if self.upper_bound is not None:
173 elements.append(_helpers.create_xml_dom_element(
174 doc, 'upper_bound', self.upper_bound))
175 return elements
177 def convert(self, argument: Text) -> _N:
178 """Returns the correct numeric value of argument.
180 Subclass must implement this method, and raise TypeError if argument is not
181 string or has the right numeric type.
183 Args:
184 argument: string argument passed in the commandline, or the numeric type.
186 Raises:
187 TypeError: Raised when argument is not a string or the right numeric type.
188 ValueError: Raised when failed to convert argument to the numeric value.
189 """
190 raise NotImplementedError
193class FloatParser(NumericParser[float]):
194 """Parser of floating point values.
196 Parsed value may be bounded to a given upper and lower bound.
197 """
198 number_article = 'a'
199 number_name = 'number'
200 syntactic_help = ' '.join((number_article, number_name))
202 def __init__(
203 self,
204 lower_bound: Optional[float] = None,
205 upper_bound: Optional[float] = None,
206 ) -> None:
207 super(FloatParser, self).__init__()
208 self.lower_bound = lower_bound
209 self.upper_bound = upper_bound
210 sh = self.syntactic_help
211 if lower_bound is not None and upper_bound is not None:
212 sh = ('%s in the range [%s, %s]' % (sh, lower_bound, upper_bound))
213 elif lower_bound == 0:
214 sh = 'a non-negative %s' % self.number_name
215 elif upper_bound == 0:
216 sh = 'a non-positive %s' % self.number_name
217 elif upper_bound is not None:
218 sh = '%s <= %s' % (self.number_name, upper_bound)
219 elif lower_bound is not None:
220 sh = '%s >= %s' % (self.number_name, lower_bound)
221 self.syntactic_help = sh
223 def convert(self, argument: Union[int, float, str]) -> float:
224 """Returns the float value of argument."""
225 if (_is_integer_type(argument) or isinstance(argument, float) or
226 isinstance(argument, str)):
227 return float(argument)
228 else:
229 raise TypeError(
230 'Expect argument to be a string, int, or float, found {}'.format(
231 type(argument)))
233 def flag_type(self) -> Text:
234 """See base class."""
235 return 'float'
238class IntegerParser(NumericParser[int]):
239 """Parser of an integer value.
241 Parsed value may be bounded to a given upper and lower bound.
242 """
243 number_article = 'an'
244 number_name = 'integer'
245 syntactic_help = ' '.join((number_article, number_name))
247 def __init__(
248 self, lower_bound: Optional[int] = None, upper_bound: Optional[int] = None
249 ) -> None:
250 super(IntegerParser, self).__init__()
251 self.lower_bound = lower_bound
252 self.upper_bound = upper_bound
253 sh = self.syntactic_help
254 if lower_bound is not None and upper_bound is not None:
255 sh = ('%s in the range [%s, %s]' % (sh, lower_bound, upper_bound))
256 elif lower_bound == 1:
257 sh = 'a positive %s' % self.number_name
258 elif upper_bound == -1:
259 sh = 'a negative %s' % self.number_name
260 elif lower_bound == 0:
261 sh = 'a non-negative %s' % self.number_name
262 elif upper_bound == 0:
263 sh = 'a non-positive %s' % self.number_name
264 elif upper_bound is not None:
265 sh = '%s <= %s' % (self.number_name, upper_bound)
266 elif lower_bound is not None:
267 sh = '%s >= %s' % (self.number_name, lower_bound)
268 self.syntactic_help = sh
270 def convert(self, argument: Union[int, Text]) -> int:
271 """Returns the int value of argument."""
272 if _is_integer_type(argument):
273 return argument
274 elif isinstance(argument, str):
275 base = 10
276 if len(argument) > 2 and argument[0] == '0':
277 if argument[1] == 'o':
278 base = 8
279 elif argument[1] == 'x':
280 base = 16
281 return int(argument, base)
282 else:
283 raise TypeError('Expect argument to be a string or int, found {}'.format(
284 type(argument)))
286 def flag_type(self) -> Text:
287 """See base class."""
288 return 'int'
291class BooleanParser(ArgumentParser[bool]):
292 """Parser of boolean values."""
294 def parse(self, argument: Union[Text, int]) -> bool:
295 """See base class."""
296 if isinstance(argument, str):
297 if argument.lower() in ('true', 't', '1'):
298 return True
299 elif argument.lower() in ('false', 'f', '0'):
300 return False
301 else:
302 raise ValueError('Non-boolean argument to boolean flag', argument)
303 elif isinstance(argument, int):
304 # Only allow bool or integer 0, 1.
305 # Note that float 1.0 == True, 0.0 == False.
306 bool_value = bool(argument)
307 if argument == bool_value:
308 return bool_value
309 else:
310 raise ValueError('Non-boolean argument to boolean flag', argument)
312 raise TypeError('Non-boolean argument to boolean flag', argument)
314 def flag_type(self) -> Text:
315 """See base class."""
316 return 'bool'
319class EnumParser(ArgumentParser[Text]):
320 """Parser of a string enum value (a string value from a given set)."""
322 def __init__(
323 self, enum_values: Iterable[Text], case_sensitive: bool = True
324 ) -> None:
325 """Initializes EnumParser.
327 Args:
328 enum_values: [str], a non-empty list of string values in the enum.
329 case_sensitive: bool, whether or not the enum is to be case-sensitive.
331 Raises:
332 ValueError: When enum_values is empty.
333 """
334 if not enum_values:
335 raise ValueError(
336 'enum_values cannot be empty, found "{}"'.format(enum_values))
337 if isinstance(enum_values, str):
338 raise ValueError(
339 'enum_values cannot be a str, found "{}"'.format(enum_values)
340 )
341 super(EnumParser, self).__init__()
342 self.enum_values = list(enum_values)
343 self.case_sensitive = case_sensitive
345 def parse(self, argument: Text) -> Text:
346 """Determines validity of argument and returns the correct element of enum.
348 Args:
349 argument: str, the supplied flag value.
351 Returns:
352 The first matching element from enum_values.
354 Raises:
355 ValueError: Raised when argument didn't match anything in enum.
356 """
357 if self.case_sensitive:
358 if argument not in self.enum_values:
359 raise ValueError('value should be one of <%s>' %
360 '|'.join(self.enum_values))
361 else:
362 return argument
363 else:
364 if argument.upper() not in [value.upper() for value in self.enum_values]:
365 raise ValueError('value should be one of <%s>' %
366 '|'.join(self.enum_values))
367 else:
368 return [value for value in self.enum_values
369 if value.upper() == argument.upper()][0]
371 def flag_type(self) -> Text:
372 """See base class."""
373 return 'string enum'
376class EnumClassParser(ArgumentParser[_ET]):
377 """Parser of an Enum class member."""
379 def __init__(
380 self, enum_class: Type[_ET], case_sensitive: bool = True
381 ) -> None:
382 """Initializes EnumParser.
384 Args:
385 enum_class: class, the Enum class with all possible flag values.
386 case_sensitive: bool, whether or not the enum is to be case-sensitive. If
387 False, all member names must be unique when case is ignored.
389 Raises:
390 TypeError: When enum_class is not a subclass of Enum.
391 ValueError: When enum_class is empty.
392 """
393 if not issubclass(enum_class, enum.Enum):
394 raise TypeError('{} is not a subclass of Enum.'.format(enum_class))
395 if not enum_class.__members__:
396 raise ValueError('enum_class cannot be empty, but "{}" is empty.'
397 .format(enum_class))
398 if not case_sensitive:
399 members = collections.Counter(
400 name.lower() for name in enum_class.__members__)
401 duplicate_keys = {
402 member for member, count in members.items() if count > 1
403 }
404 if duplicate_keys:
405 raise ValueError(
406 'Duplicate enum values for {} using case_sensitive=False'.format(
407 duplicate_keys))
409 super(EnumClassParser, self).__init__()
410 self.enum_class = enum_class
411 self._case_sensitive = case_sensitive
412 if case_sensitive:
413 self._member_names = tuple(enum_class.__members__)
414 else:
415 self._member_names = tuple(
416 name.lower() for name in enum_class.__members__)
418 @property
419 def member_names(self) -> Sequence[Text]:
420 """The accepted enum names, in lowercase if not case sensitive."""
421 return self._member_names
423 def parse(self, argument: Union[_ET, Text]) -> _ET:
424 """Determines validity of argument and returns the correct element of enum.
426 Args:
427 argument: str or Enum class member, the supplied flag value.
429 Returns:
430 The first matching Enum class member in Enum class.
432 Raises:
433 ValueError: Raised when argument didn't match anything in enum.
434 """
435 if isinstance(argument, self.enum_class):
436 return argument # pytype: disable=bad-return-type
437 elif not isinstance(argument, str):
438 raise ValueError(
439 '{} is not an enum member or a name of a member in {}'.format(
440 argument, self.enum_class))
441 key = EnumParser(
442 self._member_names, case_sensitive=self._case_sensitive).parse(argument)
443 if self._case_sensitive:
444 return self.enum_class[key]
445 else:
446 # If EnumParser.parse() return a value, we're guaranteed to find it
447 # as a member of the class
448 return next(value for name, value in self.enum_class.__members__.items()
449 if name.lower() == key.lower())
451 def flag_type(self) -> Text:
452 """See base class."""
453 return 'enum class'
456class ListSerializer(Generic[_T], ArgumentSerializer[List[_T]]):
458 def __init__(self, list_sep: Text) -> None:
459 self.list_sep = list_sep
461 def serialize(self, value: List[_T]) -> Text:
462 """See base class."""
463 return self.list_sep.join([str(x) for x in value])
466class EnumClassListSerializer(ListSerializer[_ET]):
467 """A serializer for :class:`MultiEnumClass` flags.
469 This serializer simply joins the output of `EnumClassSerializer` using a
470 provided separator.
471 """
473 def __init__(self, list_sep: Text, **kwargs) -> None:
474 """Initializes EnumClassListSerializer.
476 Args:
477 list_sep: String to be used as a separator when serializing
478 **kwargs: Keyword arguments to the `EnumClassSerializer` used to serialize
479 individual values.
480 """
481 super(EnumClassListSerializer, self).__init__(list_sep)
482 self._element_serializer = EnumClassSerializer(**kwargs)
484 def serialize(self, value: Union[_ET, List[_ET]]) -> Text:
485 """See base class."""
486 if isinstance(value, list):
487 return self.list_sep.join(
488 self._element_serializer.serialize(x) for x in value)
489 else:
490 return self._element_serializer.serialize(value)
493class CsvListSerializer(ListSerializer[Text]):
495 def serialize(self, value: List[Text]) -> Text:
496 """Serializes a list as a CSV string or unicode."""
497 output = io.StringIO()
498 writer = csv.writer(output, delimiter=self.list_sep)
499 writer.writerow([str(x) for x in value])
500 serialized_value = output.getvalue().strip()
502 # We need the returned value to be pure ascii or Unicodes so that
503 # when the xml help is generated they are usefully encodable.
504 return str(serialized_value)
507class EnumClassSerializer(ArgumentSerializer):
508 """Class for generating string representations of an enum class flag value."""
510 def __init__(self, lowercase: bool) -> None:
511 """Initializes EnumClassSerializer.
513 Args:
514 lowercase: If True, enum member names are lowercased during serialization.
515 """
516 self._lowercase = lowercase
518 def serialize(self, value: _ET) -> Text:
519 """Returns a serialized string of the Enum class value."""
520 as_string = str(value.name)
521 return as_string.lower() if self._lowercase else as_string
524class BaseListParser(ArgumentParser):
525 """Base class for a parser of lists of strings.
527 To extend, inherit from this class; from the subclass ``__init__``, call::
529 super().__init__(token, name)
531 where token is a character used to tokenize, and name is a description
532 of the separator.
533 """
535 def __init__(
536 self, token: Optional[Text] = None, name: Optional[Text] = None
537 ) -> None:
538 assert name
539 super(BaseListParser, self).__init__()
540 self._token = token
541 self._name = name
542 self.syntactic_help = 'a %s separated list' % self._name
544 def parse(self, argument: Text) -> List[Text]:
545 """See base class."""
546 if isinstance(argument, list):
547 return argument
548 elif not argument:
549 return []
550 else:
551 return [s.strip() for s in argument.split(self._token)]
553 def flag_type(self) -> Text:
554 """See base class."""
555 return '%s separated list of strings' % self._name
558class ListParser(BaseListParser):
559 """Parser for a comma-separated list of strings."""
561 def __init__(self) -> None:
562 super(ListParser, self).__init__(',', 'comma')
564 def parse(self, argument: Union[Text, List[Text]]) -> List[Text]:
565 """Parses argument as comma-separated list of strings."""
566 if isinstance(argument, list):
567 return argument
568 elif not argument:
569 return []
570 else:
571 try:
572 return [s.strip() for s in list(csv.reader([argument], strict=True))[0]]
573 except csv.Error as e:
574 # Provide a helpful report for case like
575 # --listflag="$(printf 'hello,\nworld')"
576 # IOW, list flag values containing naked newlines. This error
577 # was previously "reported" by allowing csv.Error to
578 # propagate.
579 raise ValueError('Unable to parse the value %r as a %s: %s'
580 % (argument, self.flag_type(), e))
582 def _custom_xml_dom_elements(
583 self, doc: minidom.Document
584 ) -> List[minidom.Element]:
585 elements = super(ListParser, self)._custom_xml_dom_elements(doc)
586 elements.append(_helpers.create_xml_dom_element(
587 doc, 'list_separator', repr(',')))
588 return elements
591class WhitespaceSeparatedListParser(BaseListParser):
592 """Parser for a whitespace-separated list of strings."""
594 def __init__(self, comma_compat: bool = False) -> None:
595 """Initializer.
597 Args:
598 comma_compat: bool, whether to support comma as an additional separator.
599 If False then only whitespace is supported. This is intended only for
600 backwards compatibility with flags that used to be comma-separated.
601 """
602 self._comma_compat = comma_compat
603 name = 'whitespace or comma' if self._comma_compat else 'whitespace'
604 super(WhitespaceSeparatedListParser, self).__init__(None, name)
606 def parse(self, argument: Union[Text, List[Text]]) -> List[Text]:
607 """Parses argument as whitespace-separated list of strings.
609 It also parses argument as comma-separated list of strings if requested.
611 Args:
612 argument: string argument passed in the commandline.
614 Returns:
615 [str], the parsed flag value.
616 """
617 if isinstance(argument, list):
618 return argument
619 elif not argument:
620 return []
621 else:
622 if self._comma_compat:
623 argument = argument.replace(',', ' ')
624 return argument.split()
626 def _custom_xml_dom_elements(
627 self, doc: minidom.Document
628 ) -> List[minidom.Element]:
629 elements = super(WhitespaceSeparatedListParser, self
630 )._custom_xml_dom_elements(doc)
631 separators = list(string.whitespace)
632 if self._comma_compat:
633 separators.append(',')
634 separators.sort()
635 for sep_char in separators:
636 elements.append(_helpers.create_xml_dom_element(
637 doc, 'list_separator', repr(sep_char)))
638 return elements