1# -*- coding: utf-8 -*- #
2# Copyright 2013 Google LLC. All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#    http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16"""A module that provides parsing utilities for argparse.
17
18For details of how argparse argument pasers work, see:
19
20http://docs.python.org/dev/library/argparse.html#type
21
22Example usage:
23
24import argparse
25import arg_parsers
26
27parser = argparse.ArgumentParser()
28
29parser.add_argument(
30'--metadata',
31type=arg_parsers.ArgDict())
32parser.add_argument(
33'--delay',
34default='5s',
35type=arg_parsers.Duration(lower_bound='1s', upper_bound='10s')
36parser.add_argument(
37'--disk-size',
38default='10GB',
39type=arg_parsers.BinarySize(lower_bound='1GB', upper_bound='10TB')
40
41res = parser.parse_args(
42'--names --metadata x=y,a=b,c=d --delay 1s --disk-size 10gb'.split())
43
44assert res.metadata == {'a': 'b', 'c': 'd', 'x': 'y'}
45assert res.delay == 1
46assert res.disk_size == 10737418240
47
48"""
49
50from __future__ import absolute_import
51from __future__ import division
52from __future__ import unicode_literals
53
54import argparse
55import collections
56import copy
57import re
58
59from dateutil import tz
60
61from googlecloudsdk.calliope import parser_errors
62from googlecloudsdk.core import log
63from googlecloudsdk.core import yaml
64from googlecloudsdk.core.console import console_attr
65from googlecloudsdk.core.console import console_io
66from googlecloudsdk.core.util import files
67from googlecloudsdk.core.util import times
68
69import six
70from six.moves import zip  # pylint: disable=redefined-builtin
71
72
73__all__ = ['Duration', 'BinarySize']
74
75
76class Error(Exception):
77  """Exceptions that are defined by this module."""
78
79
80class ArgumentTypeError(Error, argparse.ArgumentTypeError):
81  """Exceptions for parsers that are used as argparse types."""
82
83
84class ArgumentParsingError(Error, argparse.ArgumentError):
85  """Raised when there is a problem with user input.
86
87  argparse.ArgumentError takes both the action and a message as constructor
88  parameters.
89  """
90
91
92def _GenerateErrorMessage(error, user_input=None, error_idx=None):
93  """Constructs an error message for an exception.
94
95  Args:
96    error: str, The error message that should be displayed. This
97      message should not end with any punctuation--the full error
98      message is constructed by appending more information to error.
99    user_input: str, The user input that caused the error.
100    error_idx: int, The index at which the error occurred. If None,
101      the index will not be printed in the error message.
102
103  Returns:
104    str: The message to use for the exception.
105  """
106  if user_input is None:
107    return error
108  elif not user_input:  # Is input empty?
109    return error + '; received empty string'
110  elif error_idx is None:
111    return error + '; received: ' + user_input
112  return ('{error_message} at index {error_idx}: {user_input}'
113          .format(error_message=error, user_input=user_input,
114                  error_idx=error_idx))
115
116
117_VALUE_PATTERN = r"""
118    ^                           # Beginning of input marker.
119    (?P<amount>\d+)             # Amount.
120    ((?P<suffix>[-/a-zA-Z]+))?  # Optional scale and type abbr.
121    $                           # End of input marker.
122"""
123
124_RANGE_PATTERN = r'^(?P<start>[0-9]+)(-(?P<end>[0-9]+))?$'
125
126_SECOND = 1
127_MINUTE = 60 * _SECOND
128_HOUR = 60 * _MINUTE
129_DAY = 24 * _HOUR
130
131# The units are adopted from sleep(1):
132#   http://linux.die.net/man/1/sleep
133_DURATION_SCALES = {
134    's': _SECOND,
135    'm': _MINUTE,
136    'h': _HOUR,
137    'd': _DAY,
138}
139
140_BINARY_SIZE_SCALES = {
141    '': 1,
142    'K': 1 << 10,
143    'M': 1 << 20,
144    'G': 1 << 30,
145    'T': 1 << 40,
146    'P': 1 << 50,
147    'Ki': 1 << 10,
148    'Mi': 1 << 20,
149    'Gi': 1 << 30,
150    'Ti': 1 << 40,
151    'Pi': 1 << 50,
152}
153
154
155def GetMultiCompleter(individual_completer):
156  """Create a completer to handle completion for comma separated lists.
157
158  Args:
159    individual_completer: A function that completes an individual element.
160
161  Returns:
162    A function that completes the last element of the list.
163  """
164  def MultiCompleter(prefix, parsed_args, **kwargs):
165    start = ''
166    lst = prefix.rsplit(',', 1)
167    if len(lst) > 1:
168      start = lst[0] + ','
169      prefix = lst[1]
170    matches = individual_completer(prefix, parsed_args, **kwargs)
171    return [start + match for match in matches]
172  return MultiCompleter
173
174
175def _DeleteTypeAbbr(suffix, type_abbr='B'):
176  """Returns suffix with trailing type abbreviation deleted."""
177  if not suffix:
178    return suffix
179  s = suffix.upper()
180  i = len(s)
181  for c in reversed(type_abbr.upper()):
182    if not i:
183      break
184    if s[i - 1] == c:
185      i -= 1
186  return suffix[:i]
187
188
189def GetBinarySizePerUnit(suffix, type_abbr='B'):
190  """Returns the binary size per unit for binary suffix string.
191
192  Args:
193    suffix: str, A case insensitive unit suffix string with optional type
194      abbreviation.
195    type_abbr: str, The optional case insensitive type abbreviation following
196      the suffix.
197
198  Raises:
199    ValueError for unknown units.
200
201  Returns:
202    The binary size per unit for a unit+type_abbr suffix.
203  """
204  unit = _DeleteTypeAbbr(suffix.upper(), type_abbr)
205  return _BINARY_SIZE_SCALES.get(unit)
206
207
208def _ValueParser(scales, default_unit, lower_bound=None, upper_bound=None,
209                 strict_case=True, type_abbr='B',
210                 suggested_binary_size_scales=None):
211  """A helper that returns a function that can parse values with units.
212
213  Casing for all units matters.
214
215  Args:
216    scales: {str: int}, A dictionary mapping units to their magnitudes in
217      relation to the lowest magnitude unit in the dict.
218    default_unit: str, The default unit to use if the user's input is
219      missing unit.
220    lower_bound: str, An inclusive lower bound.
221    upper_bound: str, An inclusive upper bound.
222    strict_case: bool, whether to be strict on case-checking
223    type_abbr: str, the type suffix abbreviation, e.g., B for bytes, b/s for
224      bits/sec.
225    suggested_binary_size_scales: list, A list of strings with units that will
226                                    be recommended to user.
227
228  Returns:
229    A function that can parse values.
230  """
231
232  def UnitsByMagnitude(suggested_binary_size_scales=None):
233    """Returns a list of the units in scales sorted by magnitude."""
234    scale_items = sorted(six.iteritems(scales),
235                         key=lambda value: (value[1], value[0]))
236    if suggested_binary_size_scales is None:
237      return [key + type_abbr for key, _ in scale_items]
238    return [key + type_abbr for key, _ in scale_items
239            if key + type_abbr in suggested_binary_size_scales]
240
241  def Parse(value):
242    """Parses value that can contain a unit and type avvreviation."""
243    match = re.match(_VALUE_PATTERN, value, re.VERBOSE)
244    if not match:
245      raise ArgumentTypeError(_GenerateErrorMessage(
246          'given value must be of the form INTEGER[UNIT] where units '
247          'can be one of {0}'
248          .format(', '.join(UnitsByMagnitude(suggested_binary_size_scales))),
249          user_input=value))
250
251    amount = int(match.group('amount'))
252    suffix = match.group('suffix') or ''
253    unit = _DeleteTypeAbbr(suffix, type_abbr)
254    if strict_case:
255      unit_case = unit
256      default_unit_case = _DeleteTypeAbbr(default_unit, type_abbr)
257      scales_case = scales
258    else:
259      unit_case = unit.upper()
260      default_unit_case = _DeleteTypeAbbr(default_unit.upper(), type_abbr)
261      scales_case = dict([(k.upper(), v) for k, v in scales.items()])
262
263    if not unit and unit == suffix:
264      return amount * scales_case[default_unit_case]
265    elif unit_case in scales_case:
266      return amount * scales_case[unit_case]
267    else:
268      raise ArgumentTypeError(_GenerateErrorMessage(
269          'unit must be one of {0}'.format(', '.join(UnitsByMagnitude())),
270          user_input=unit))
271
272  if lower_bound is None:
273    parsed_lower_bound = None
274  else:
275    parsed_lower_bound = Parse(lower_bound)
276
277  if upper_bound is None:
278    parsed_upper_bound = None
279  else:
280    parsed_upper_bound = Parse(upper_bound)
281
282  def ParseWithBoundsChecking(value):
283    """Same as Parse except bound checking is performed."""
284    if value is None:
285      return None
286    else:
287      parsed_value = Parse(value)
288      if parsed_lower_bound is not None and parsed_value < parsed_lower_bound:
289        raise ArgumentTypeError(_GenerateErrorMessage(
290            'value must be greater than or equal to {0}'.format(lower_bound),
291            user_input=value))
292      elif parsed_upper_bound is not None and parsed_value > parsed_upper_bound:
293        raise ArgumentTypeError(_GenerateErrorMessage(
294            'value must be less than or equal to {0}'.format(upper_bound),
295            user_input=value))
296      else:
297        return parsed_value
298
299  return ParseWithBoundsChecking
300
301
302def RegexpValidator(pattern, description):
303  """Returns a function that validates a string against a regular expression.
304
305  For example:
306
307  >>> alphanumeric_type = RegexpValidator(
308  ...   r'[a-zA-Z0-9]+',
309  ...   'must contain one or more alphanumeric characters')
310  >>> parser.add_argument('--foo', type=alphanumeric_type)
311  >>> parser.parse_args(['--foo', '?'])
312  >>> # SystemExit raised and the error "error: argument foo: Bad value [?]:
313  >>> # must contain one or more alphanumeric characters" is displayed
314
315  Args:
316    pattern: str, the pattern to compile into a regular expression to check
317    description: an error message to show if the argument doesn't match
318
319  Returns:
320    function: str -> str, usable as an argparse type
321  """
322  def Parse(value):
323    if not re.match(pattern + '$', value):
324      raise ArgumentTypeError('Bad value [{0}]: {1}'.format(value, description))
325    return value
326  return Parse
327
328
329def CustomFunctionValidator(fn, description, parser=None):
330  """Returns a function that validates the input by running it through fn.
331
332  For example:
333
334  >>> def isEven(val):
335  ...   return val % 2 == 0
336  >>> even_number_parser = arg_parsers.CustomFunctionValidator(
337        isEven, 'This is not even!', parser=arg_parsers.BoundedInt(0))
338  >>> parser.add_argument('--foo', type=even_number_parser)
339  >>> parser.parse_args(['--foo', '3'])
340  >>> # SystemExit raised and the error "error: argument foo: Bad value [3]:
341  >>> # This is not even!" is displayed
342
343  Args:
344    fn: str -> boolean
345    description: an error message to show if boolean function returns False
346    parser: an arg_parser that is applied to to value before validation. The
347      value is also returned by this parser.
348
349  Returns:
350    function: str -> str, usable as an argparse type
351  """
352
353  def Parse(value):
354    """Validates and returns a custom object from an argument string value."""
355    try:
356      parsed_value = parser(value) if parser else value
357    except ArgumentTypeError:
358      pass
359    else:
360      if fn(parsed_value):
361        return parsed_value
362    encoded_value = console_attr.SafeText(value)
363    formatted_err = 'Bad value [{0}]: {1}'.format(encoded_value, description)
364    raise ArgumentTypeError(formatted_err)
365
366  return Parse
367
368
369def Duration(default_unit='s',
370             lower_bound='0',
371             upper_bound=None,
372             parsed_unit='s'):
373  """Returns a function that can parse time durations.
374
375  See times.ParseDuration() for details. If the unit is omitted, seconds is
376  assumed. The parsed unit is assumed to be seconds, but can be specified as
377  ms or us.
378  For example:
379
380    parser = Duration()
381    assert parser('10s') == 10
382    parser = Duration(parsed_unit='ms')
383    assert parser('10s') == 10000
384    parser = Duration(parsed_unit='us')
385    assert parser('10s') == 10000000
386
387  Args:
388    default_unit: str, The default duration unit.
389    lower_bound: str, An inclusive lower bound for values.
390    upper_bound: str, An inclusive upper bound for values.
391    parsed_unit: str, The unit that the result should be returned as. Can be
392    's', 'ms', or 'us'.
393
394  Raises:
395    ArgumentTypeError: If either the lower_bound or upper_bound
396      cannot be parsed. The returned function will also raise this
397      error if it cannot parse its input. This exception is also
398      raised if the returned function receives an out-of-bounds
399      input.
400
401  Returns:
402    A function that accepts a single time duration as input to be
403      parsed.
404  """
405
406  def Parse(value):
407    """Parses a duration from value and returns integer of the parsed_unit."""
408    if parsed_unit == 'ms':
409      multiplier = 1000
410    elif parsed_unit == 'us':
411      multiplier = 1000000
412    elif parsed_unit == 's':
413      multiplier = 1
414    else:
415      raise ArgumentTypeError(
416          _GenerateErrorMessage('parsed_unit must be one of s, ms, us.'))
417    try:
418      duration = times.ParseDuration(value, default_suffix=default_unit)
419      return int(duration.total_seconds * multiplier)
420    except times.Error as e:
421      message = six.text_type(e).rstrip('.')
422      raise ArgumentTypeError(_GenerateErrorMessage(
423          'Failed to parse duration: {0}'.format(message, user_input=value)))
424
425  parsed_lower_bound = Parse(lower_bound)
426
427  if upper_bound is None:
428    parsed_upper_bound = None
429  else:
430    parsed_upper_bound = Parse(upper_bound)
431
432  def ParseWithBoundsChecking(value):
433    """Same as Parse except bound checking is performed."""
434    if value is None:
435      return None
436    parsed_value = Parse(value)
437    if parsed_lower_bound is not None and parsed_value < parsed_lower_bound:
438      raise ArgumentTypeError(_GenerateErrorMessage(
439          'value must be greater than or equal to {0}'.format(lower_bound),
440          user_input=value))
441    if parsed_upper_bound is not None and parsed_value > parsed_upper_bound:
442      raise ArgumentTypeError(_GenerateErrorMessage(
443          'value must be less than or equal to {0}'.format(upper_bound),
444          user_input=value))
445    return parsed_value
446
447  return ParseWithBoundsChecking
448
449
450def BinarySize(lower_bound=None, upper_bound=None,
451               suggested_binary_size_scales=None, default_unit='G',
452               type_abbr='B'):
453  """Returns a function that can parse binary sizes.
454
455  Binary sizes are defined as base-2 values representing number of
456  bytes.
457
458  Input to the parsing function must be a string of the form:
459
460    INTEGER[UNIT]
461
462  The integer must be non-negative. Valid units are "B", "KB", "MB",
463  "GB", "TB", "KiB", "MiB", "GiB", "TiB", "PiB".  If the unit is
464  omitted then default_unit is assumed.
465
466  The result is parsed in bytes. For example:
467
468    parser = BinarySize()
469    assert parser('10GB') == 1073741824
470
471  Args:
472    lower_bound: str, An inclusive lower bound for values.
473    upper_bound: str, An inclusive upper bound for values.
474    suggested_binary_size_scales: list, A list of strings with units that will
475                                    be recommended to user.
476    default_unit: str, unit used when user did not specify unit.
477    type_abbr: str, the type suffix abbreviation, e.g., B for bytes, b/s for
478      bits/sec.
479
480  Raises:
481    ArgumentTypeError: If either the lower_bound or upper_bound
482      cannot be parsed. The returned function will also raise this
483      error if it cannot parse its input. This exception is also
484      raised if the returned function receives an out-of-bounds
485      input.
486
487  Returns:
488    A function that accepts a single binary size as input to be
489      parsed.
490  """
491  return _ValueParser(
492      _BINARY_SIZE_SCALES, default_unit=default_unit, lower_bound=lower_bound,
493      upper_bound=upper_bound, strict_case=False, type_abbr=type_abbr,
494      suggested_binary_size_scales=suggested_binary_size_scales)
495
496
497_KV_PAIR_DELIMITER = '='
498
499
500class Range(object):
501  """Range of integer values."""
502
503  def __init__(self, start, end):
504    self.start = start
505    self.end = end
506
507  @staticmethod
508  def Parse(string_value):
509    """Creates Range object out of given string value."""
510    match = re.match(_RANGE_PATTERN, string_value)
511    if not match:
512      raise ArgumentTypeError('Expected a non-negative integer value or a '
513                              'range of such values instead of "{0}"'
514                              .format(string_value))
515    start = int(match.group('start'))
516    end = match.group('end')
517    if end is None:
518      end = start
519    else:
520      end = int(end)
521    if end < start:
522      raise ArgumentTypeError('Expected range start {0} smaller or equal to '
523                              'range end {1} in "{2}"'.format(
524                                  start, end, string_value))
525    return Range(start, end)
526
527  def Combine(self, other):
528    """Combines two overlapping or adjacent ranges, raises otherwise."""
529    if self.end + 1 < other.start or self.start > other.end + 1:
530      raise Error('Cannot combine non-overlapping or non-adjacent ranges '
531                  '{0} and {1}'.format(self, other))
532    return Range(min(self.start, other.start), max(self.end, other.end))
533
534  def __eq__(self, other):
535    if isinstance(other, Range):
536      return self.start == other.start and self.end == other.end
537    return False
538
539  def __lt__(self, other):
540    if self.start == other.start:
541      return self.end < other.end
542    return self.start < other.start
543
544  def __str__(self):
545    if self.start == self.end:
546      return six.text_type(self.start)
547    return '{0}-{1}'.format(self.start, self.end)
548
549
550class HostPort(object):
551  """A class for holding host and port information."""
552
553  IPV4_OR_HOST_PATTERN = r'^(?P<address>[\w\d\.-]+)?(:|:(?P<port>[\d]+))?$'
554  # includes hostnames
555  IPV6_PATTERN = r'^(\[(?P<address>[\w\d:]+)\])(:|:(?P<port>[\d]+))?$'
556
557  def __init__(self, host, port):
558    self.host = host
559    self.port = port
560
561  @staticmethod
562  def Parse(s, ipv6_enabled=False):
563    """Parse the given string into a HostPort object.
564
565    This can be used as an argparse type.
566
567    Args:
568      s: str, The string to parse. If ipv6_enabled and host is an IPv6 address,
569      it should be placed in square brackets: e.g.
570        [2001:db8:0:0:0:ff00:42:8329]
571        or
572        [2001:db8:0:0:0:ff00:42:8329]:8080
573      ipv6_enabled: boolean, If True then accept IPv6 addresses.
574
575    Raises:
576      ArgumentTypeError: If the string is not valid.
577
578    Returns:
579      HostPort, The parsed object.
580    """
581    if not s:
582      return HostPort(None, None)
583
584    match = re.match(HostPort.IPV4_OR_HOST_PATTERN, s, re.UNICODE)
585    if ipv6_enabled and not match:
586      match = re.match(HostPort.IPV6_PATTERN, s, re.UNICODE)
587      if not match:
588        raise ArgumentTypeError(_GenerateErrorMessage(
589            'Failed to parse host and port. Expected format \n\n'
590            '  IPv4_ADDRESS_OR_HOSTNAME:PORT\n\n'
591            'or\n\n'
592            '  [IPv6_ADDRESS]:PORT\n\n'
593            '(where :PORT is optional).',
594            user_input=s))
595    elif not match:
596      raise ArgumentTypeError(_GenerateErrorMessage(
597          'Failed to parse host and port. Expected format \n\n'
598          '  IPv4_ADDRESS_OR_HOSTNAME:PORT\n\n'
599          '(where :PORT is optional).',
600          user_input=s))
601    return HostPort(match.group('address'), match.group('port'))
602
603
604class Day(object):
605  """A class for parsing a datetime object for a specific day."""
606
607  @staticmethod
608  def Parse(s):
609    if not s:
610      return None
611    try:
612      return times.ParseDateTime(s, '%Y-%m-%d').date()
613    except times.Error as e:
614      raise ArgumentTypeError(
615          _GenerateErrorMessage(
616              'Failed to parse date: {0}'.format(six.text_type(e)),
617              user_input=s))
618
619
620class Datetime(object):
621  """A class for parsing a datetime object."""
622
623  @staticmethod
624  def Parse(s):
625    """Parses a string value into a Datetime object in local timezone."""
626    if not s:
627      return None
628    try:
629      return times.ParseDateTime(s)
630    except times.Error as e:
631      raise ArgumentTypeError(
632          _GenerateErrorMessage(
633              'Failed to parse date/time: {0}'.format(six.text_type(e)),
634              user_input=s))
635
636  @staticmethod
637  def ParseUtcTime(s):
638    """Parses a string representing a time in UTC into a Datetime object."""
639    if not s:
640      return None
641    try:
642      return times.ParseDateTime(s, tzinfo=tz.tzutc())
643    except times.Error as e:
644      raise ArgumentTypeError(
645          _GenerateErrorMessage(
646              'Failed to parse UTC time: {0}'.format(six.text_type(e)),
647              user_input=s))
648
649
650class DayOfWeek(object):
651  """A class for parsing a day of the week."""
652
653  DAYS = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT']
654
655  @staticmethod
656  def Parse(s):
657    """Validates and normalizes a string as a day of the week."""
658    if not s:
659      return None
660    fixed = s.upper()[:3]
661    if fixed not in DayOfWeek.DAYS:
662      raise ArgumentTypeError(
663          _GenerateErrorMessage(
664              'Failed to parse day of week. Value should be one of {0}'.format(
665                  ', '.join(DayOfWeek.DAYS)),
666              user_input=s))
667    return fixed
668
669
670def _BoundedType(type_builder, type_description,
671                 lower_bound=None, upper_bound=None, unlimited=False):
672  """Returns a function that can parse given type within some bound.
673
674  Args:
675    type_builder: A callable for building the requested type from the value
676        string.
677    type_description: str, Description of the requested type (for verbose
678        messages).
679    lower_bound: of type compatible with type_builder,
680        The value must be >= lower_bound.
681    upper_bound: of type compatible with type_builder,
682        The value must be <= upper_bound.
683    unlimited: bool, If True then a value of 'unlimited' means no limit.
684
685  Returns:
686    A function that can parse given type within some bound.
687  """
688
689  def Parse(value):
690    """Parses value as a type constructed by type_builder.
691
692    Args:
693      value: str, Value to be converted to the requested type.
694
695    Raises:
696      ArgumentTypeError: If the provided value is out of bounds or unparsable.
697
698    Returns:
699      Value converted to the requested type.
700    """
701    if unlimited and value == 'unlimited':
702      return None
703
704    try:
705      v = type_builder(value)
706    except ValueError:
707      raise ArgumentTypeError(
708          _GenerateErrorMessage('Value must be {0}'.format(type_description),
709                                user_input=value))
710
711    if lower_bound is not None and v < lower_bound:
712      raise ArgumentTypeError(
713          _GenerateErrorMessage(
714              'Value must be greater than or equal to {0}'.format(lower_bound),
715              user_input=value))
716
717    if upper_bound is not None and upper_bound < v:
718      raise ArgumentTypeError(
719          _GenerateErrorMessage(
720              'Value must be less than or equal to {0}'.format(upper_bound),
721              user_input=value))
722
723    return v
724
725  return Parse
726
727
728def BoundedInt(*args, **kwargs):
729  return _BoundedType(int, 'an integer', *args, **kwargs)
730
731
732def BoundedFloat(*args, **kwargs):
733  return _BoundedType(float, 'a floating point number', *args, **kwargs)
734
735
736def _TokenizeQuotedList(arg_value, delim=','):
737  """Tokenize an argument into a list.
738
739  Args:
740    arg_value: str, The raw argument.
741    delim: str, The delimiter on which to split the argument string.
742
743  Returns:
744    [str], The tokenized list.
745  """
746  if arg_value:
747    if not arg_value.endswith(delim):
748      arg_value += delim
749    return arg_value.split(delim)[:-1]
750  return []
751
752
753class ArgType(object):
754  """Base class for arg types."""
755
756
757class ArgBoolean(ArgType):
758  """Interpret an argument value as a bool."""
759
760  def __init__(
761      self, truthy_strings=None, falsey_strings=None, case_sensitive=False):
762    self._case_sensitive = case_sensitive
763    if truthy_strings:
764      self._truthy_strings = truthy_strings
765    else:
766      self._truthy_strings = ['true', 'yes']
767    if falsey_strings:
768      self._falsey_strings = falsey_strings
769    else:
770      self._falsey_strings = ['false', 'no']
771
772  def __call__(self, arg_value):
773    if not self._case_sensitive:
774      normalized_arg_value = arg_value.lower()
775    else:
776      normalized_arg_value = arg_value
777    if normalized_arg_value in self._truthy_strings:
778      return True
779    if normalized_arg_value in self._falsey_strings:
780      return False
781    raise ArgumentTypeError(
782        'Invalid flag value [{0}], expected one of [{1}]'.format(
783            arg_value,
784            ', '.join(self._truthy_strings + self._falsey_strings)
785        )
786    )
787
788
789class ArgList(ArgType):
790  """Interpret an argument value as a list.
791
792  Intended to be used as the type= for a flag argument. Splits the string on
793  commas or another delimiter and returns a list.
794
795  By default, splits on commas:
796      'a,b,c' -> ['a', 'b', 'c']
797  There is an available syntax for using an alternate delimiter:
798      '^:^a,b:c' -> ['a,b', 'c']
799      '^::^a:b::c' -> ['a:b', 'c']
800      '^,^^a^,b,c' -> ['^a^', ',b', 'c']
801  """
802
803  DEFAULT_DELIM_CHAR = ','
804  ALT_DELIM_CHAR = '^'
805
806  def __init__(self,
807               element_type=None,
808               min_length=0,
809               max_length=None,
810               choices=None,
811               custom_delim_char=None,
812               visible_choices=None):
813    """Initialize an ArgList.
814
815    Args:
816      element_type: (str)->str, A function to apply to each of the list items.
817      min_length: int, The minimum size of the list.
818      max_length: int, The maximum size of the list.
819      choices: [element_type], a list of valid possibilities for elements. If
820          None, then no constraints are imposed.
821      custom_delim_char: char, A customized delimiter character.
822      visible_choices: [element_type], a list of valid possibilities for
823          elements to be shown to the user. If None, defaults to choices.
824
825    Returns:
826      (str)->[str], A function to parse the list of values in the argument.
827
828    Raises:
829      ArgumentTypeError: If the list is malformed.
830    """
831    self.element_type = element_type
832    self.choices = choices
833    self.visible_choices = (
834        visible_choices if visible_choices is not None else choices)
835
836    if self.visible_choices:
837      def ChoiceType(raw_value):
838        if element_type:
839          typed_value = element_type(raw_value)
840        else:
841          typed_value = raw_value
842        if typed_value not in choices:
843          raise ArgumentTypeError('{value} must be one of [{choices}]'.format(
844              value=typed_value,
845              choices=', '.join(
846                  [six.text_type(choice) for choice in self.visible_choices])))
847        return typed_value
848      self.element_type = ChoiceType
849
850    self.min_length = min_length
851    self.max_length = max_length
852
853    self.custom_delim_char = custom_delim_char
854
855  def __call__(self, arg_value):  # pylint:disable=missing-docstring
856
857    if isinstance(arg_value, list):
858      arg_list = arg_value
859    elif not isinstance(arg_value, six.string_types):
860      raise ArgumentTypeError('Invalid type [{}] for flag value [{}]'.format(
861          type(arg_value).__name__, arg_value))
862    else:
863      delim = self.custom_delim_char or self.DEFAULT_DELIM_CHAR
864      if (arg_value.startswith(self.ALT_DELIM_CHAR) and
865          self.ALT_DELIM_CHAR in arg_value[1:]):
866        delim, arg_value = arg_value[1:].split(self.ALT_DELIM_CHAR, 1)
867        if not delim:
868          raise ArgumentTypeError(
869              'Invalid delimeter. Please see `gcloud topic flags-file` or '
870              '`gcloud topic escaping` for information on providing list or '
871              'dictionary flag values with special characters.')
872      arg_list = _TokenizeQuotedList(arg_value, delim=delim)
873
874    # TODO(b/35944028): These exceptions won't present well to the user.
875    if len(arg_list) < self.min_length:
876      raise ArgumentTypeError('not enough args')
877    if self.max_length is not None and len(arg_list) > self.max_length:
878      raise ArgumentTypeError('too many args')
879
880    if self.element_type:
881      arg_list = [self.element_type(arg) for arg in arg_list]
882
883    return arg_list
884
885  _MAX_METAVAR_LENGTH = 30  # arbitrary, but this is pretty long
886
887  def GetUsageMsg(self, is_custom_metavar, metavar):
888    """Get a specially-formatted metavar for the ArgList to use in help.
889
890    An example is worth 1,000 words:
891
892    >>> ArgList().GetUsageMetavar('FOO')
893    '[FOO,...]'
894    >>> ArgList(min_length=1).GetUsageMetavar('FOO')
895    'FOO,[FOO,...]'
896    >>> ArgList(max_length=2).GetUsageMetavar('FOO')
897    'FOO,[FOO]'
898    >>> ArgList(max_length=3).GetUsageMetavar('FOO')  # One, two, many...
899    'FOO,[FOO,...]'
900    >>> ArgList(min_length=2, max_length=2).GetUsageMetavar('FOO')
901    'FOO,FOO'
902    >>> ArgList().GetUsageMetavar('REALLY_VERY_QUITE_LONG_METAVAR')
903    'REALLY_VERY_QUITE_LONG_METAVAR,[...]'
904
905    Args:
906      is_custom_metavar: unused in GetUsageMsg
907      metavar: string, the base metavar to turn into an ArgList metavar
908
909    Returns:
910      string, the ArgList usage metavar
911    """
912    del is_custom_metavar  # Unused in GetUsageMsg
913
914    delim_char = self.custom_delim_char or self.DEFAULT_DELIM_CHAR
915    required = delim_char.join([metavar] * self.min_length)
916
917    if self.max_length:
918      num_optional = self.max_length - self.min_length
919    else:
920      num_optional = None
921
922    # Use the "1, 2, many" approach to counting
923    if num_optional == 0:
924      optional = ''
925    elif num_optional == 1:
926      optional = '[{}]'.format(metavar)
927    elif num_optional == 2:
928      optional = '[{0}{1}[{0}]]'.format(metavar, delim_char)
929    else:
930      optional = '[{}{}...]'.format(metavar, delim_char)
931
932    msg = delim_char.join([x for x in [required, optional] if x])
933
934    if len(msg) < self._MAX_METAVAR_LENGTH:
935      return msg
936
937    # With long metavars, only put it in once.
938    if self.min_length == 0:
939      return '[{}{}...]'.format(metavar, delim_char)
940    if self.min_length == 1:
941      return '{}{}[...]'.format(metavar, delim_char)
942    else:
943      return '{0}{1}...{1}[...]'.format(metavar, delim_char)
944
945
946class ArgDict(ArgList):
947  """Interpret an argument value as a dict.
948
949  Intended to be used as the type= for a flag argument. Splits the string on
950  commas to get a list, and then splits the items on equals to get a set of
951  key-value pairs to get a dict.
952  """
953
954  def __init__(self, key_type=None, value_type=None, spec=None, min_length=0,
955               max_length=None, allow_key_only=False, required_keys=None,
956               operators=None):
957    """Initialize an ArgDict.
958
959    Args:
960      key_type: (str)->str, A function to apply to each of the dict keys.
961      value_type: (str)->str, A function to apply to each of the dict values.
962      spec: {str: (str)->str}, A mapping of expected keys to functions.
963        The functions are applied to the values. If None, an arbitrary
964        set of keys will be accepted. If not None, it is an error for the
965        user to supply a key that is not in the spec. If the function specified
966        is None, then accept a key only without '=value'.
967      min_length: int, The minimum number of keys in the dict.
968      max_length: int, The maximum number of keys in the dict.
969      allow_key_only: bool, Allow empty values.
970      required_keys: [str], Required keys in the dict.
971      operators: operator_char -> value_type, Define multiple single character
972        operators, each with its own value_type converter. Use value_type==None
973        for no conversion. The default value is {'=': value_type}
974
975    Returns:
976      (str)->{str:str}, A function to parse the dict in the argument.
977
978    Raises:
979      ArgumentTypeError: If the list is malformed.
980      ValueError: If both value_type and spec are provided.
981    """
982    super(ArgDict, self).__init__(min_length=min_length, max_length=max_length)
983    if spec and value_type:
984      raise ValueError('cannot have both spec and sub_type')
985    self.key_type = key_type
986    self.spec = spec
987    self.allow_key_only = allow_key_only
988    self.required_keys = required_keys or []
989    if not operators:
990      operators = {'=': value_type}
991    for op in operators.keys():
992      if len(op) != 1:
993        raise ArgumentTypeError(
994            'Operator [{}] must be one character.'.format(op))
995    ops = ''.join(six.iterkeys(operators))
996    key_op_value_pattern = '([^{ops}]+)([{ops}]?)(.*)'.format(
997        ops=re.escape(ops))
998    self.key_op_value = re.compile(key_op_value_pattern, re.DOTALL)
999    self.operators = operators
1000
1001  def _ApplySpec(self, key, value):
1002    if key in self.spec:
1003      if self.spec[key] is None:
1004        if value:
1005          raise ArgumentTypeError('Key [{0}] does not take a value'.format(key))
1006        return None
1007      return self.spec[key](value)
1008    else:
1009      raise ArgumentTypeError(
1010          _GenerateErrorMessage(
1011              'valid keys are [{0}]'.format(
1012                  ', '.join(sorted(self.spec.keys()))),
1013              user_input=key))
1014
1015  def _ValidateKeyValue(self, key, value, op='='):
1016    """Converts and validates <key,value> and returns (key,value)."""
1017    if (not op or value is None) and not self.allow_key_only:
1018      raise ArgumentTypeError(
1019          'Bad syntax for dict arg: [{0}]. Please see '
1020          '`gcloud topic flags-file` or `gcloud topic escaping` for '
1021          'information on providing list or dictionary flag values with '
1022          'special characters.'.format(key))
1023    if self.key_type:
1024      try:
1025        key = self.key_type(key)
1026      except ValueError:
1027        raise ArgumentTypeError('Invalid key [{0}]'.format(key))
1028    convert_value = self.operators.get(op, None)
1029    if convert_value:
1030      try:
1031        value = convert_value(value)
1032      except ValueError:
1033        raise ArgumentTypeError('Invalid value [{0}]'.format(value))
1034    if self.spec:
1035      value = self._ApplySpec(key, value)
1036    return key, value
1037
1038  def __call__(self, arg_value):  # pylint:disable=missing-docstring
1039
1040    if isinstance(arg_value, dict):
1041      raw_dict = arg_value
1042      arg_dict = collections.OrderedDict()
1043      for key, value in six.iteritems(raw_dict):
1044        key, value = self._ValidateKeyValue(key, value)
1045        arg_dict[key] = value
1046    elif not isinstance(arg_value, six.string_types):
1047      raise ArgumentTypeError('Invalid type [{}] for flag value [{}]'.format(
1048          type(arg_value).__name__, arg_value))
1049    else:
1050      arg_list = super(ArgDict, self).__call__(arg_value)
1051      arg_dict = collections.OrderedDict()
1052      for arg in arg_list:
1053        match = self.key_op_value.match(arg)
1054        # TODO(b/35944028): These exceptions won't present well to the user.
1055        if not match:
1056          raise ArgumentTypeError('Invalid flag value [{0}]'.format(arg))
1057        key, op, value = match.group(1), match.group(2), match.group(3)
1058        key, value = self._ValidateKeyValue(key, value, op=op)
1059        arg_dict[key] = value
1060
1061    for required_key in self.required_keys:
1062      if required_key not in arg_dict:
1063        raise ArgumentTypeError(
1064            'Key [{0}] required in dict arg but not provided'.format(
1065                required_key))
1066
1067    return arg_dict
1068
1069  def GetUsageMsg(self, is_custom_metavar, metavar):
1070    # If we're not using a spec to limit the key values or if metavar
1071    # has been overridden, then use the normal ArgList formatting
1072    if not self.spec or is_custom_metavar:
1073      return super(ArgDict, self).GetUsageMsg(is_custom_metavar, metavar)
1074
1075    msg_list = []
1076    spec_list = sorted(six.iteritems(self.spec))
1077
1078    # First put the spec keys with no value followed by those that expect a
1079    # value
1080    for spec_key, spec_function in spec_list:
1081      if spec_function is None:
1082        if not self.allow_key_only:
1083          raise ArgumentTypeError(
1084              'Key [{0}] specified in spec without a function but '
1085              'allow_key_only is set to False'.format(spec_key))
1086        msg_list.append(spec_key)
1087
1088    for spec_key, spec_function in spec_list:
1089      if spec_function is not None:
1090        msg_list.append('{0}={1}'.format(spec_key, spec_key.upper()))
1091
1092    msg = '[' + '],['.join(msg_list) + ']'
1093    return msg
1094
1095
1096class UpdateAction(argparse.Action):
1097  r"""Create a single dict value from delimited or repeated flags.
1098
1099  This class is intended to be a more flexible version of
1100  argparse._AppendAction.
1101
1102  For example, with the following flag definition:
1103
1104      parser.add_argument(
1105        '--inputs',
1106        type=arg_parsers.ArgDict(),
1107        action='append')
1108
1109  a caller can specify on the command line flags such as:
1110
1111    --inputs k1=v1,k2=v2
1112
1113  and the result will be a list of one dict:
1114
1115    [{ 'k1': 'v1', 'k2': 'v2' }]
1116
1117  Specifying two separate command line flags such as:
1118
1119    --inputs k1=v1 \
1120    --inputs k2=v2
1121
1122  will produce a list of dicts:
1123
1124    [{ 'k1': 'v1'}, { 'k2': 'v2' }]
1125
1126  The UpdateAction class allows for both of the above user inputs to result
1127  in the same: a single dictionary:
1128
1129    { 'k1': 'v1', 'k2': 'v2' }
1130
1131  This gives end-users a lot more flexibility in constructing their command
1132  lines, especially when scripting calls.
1133
1134  Note that this class will raise an exception if a key value is specified
1135  more than once. To allow for a key value to be specified multiple times,
1136  use UpdateActionWithAppend.
1137  """
1138
1139  def OnDuplicateKeyRaiseError(self, key, existing_value=None, new_value=None):
1140    if existing_value is None:
1141      user_input = None
1142    else:
1143      user_input = ', '.join([existing_value, new_value])
1144    raise argparse.ArgumentError(self, _GenerateErrorMessage(
1145        '"{0}" cannot be specified multiple times'.format(key),
1146        user_input=user_input))
1147
1148  def __init__(self,
1149               option_strings,
1150               dest,
1151               nargs=None,
1152               const=None,
1153               default=None,
1154               type=None,  # pylint:disable=redefined-builtin
1155               choices=None,
1156               required=False,
1157               help=None,  # pylint:disable=redefined-builtin
1158               metavar=None,
1159               onduplicatekey_handler=OnDuplicateKeyRaiseError):
1160    if nargs == 0:
1161      raise ValueError('nargs for append actions must be > 0; if arg '
1162                       'strings are not supplying the value to append, '
1163                       'the append const action may be more appropriate')
1164    if const is not None and nargs != argparse.OPTIONAL:
1165      raise ValueError('nargs must be %r to supply const' % argparse.OPTIONAL)
1166    self.choices = choices
1167    if isinstance(choices, dict):
1168      choices = sorted(choices.keys())
1169    super(UpdateAction, self).__init__(
1170        option_strings=option_strings,
1171        dest=dest,
1172        nargs=nargs,
1173        const=const,
1174        default=default,
1175        type=type,
1176        choices=choices,
1177        required=required,
1178        help=help,
1179        metavar=metavar)
1180    self.onduplicatekey_handler = onduplicatekey_handler
1181
1182  def _EnsureValue(self, namespace, name, value):
1183    if getattr(namespace, name, None) is None:
1184      setattr(namespace, name, value)
1185    return getattr(namespace, name)
1186
1187  # pylint: disable=protected-access
1188  def __call__(self, parser, namespace, values, option_string=None):
1189
1190    if isinstance(values, dict):
1191      # Get the existing arg value (if any)
1192      items = copy.copy(self._EnsureValue(
1193          namespace, self.dest, collections.OrderedDict()))
1194      # Merge the new key/value pair(s) in
1195      for k, v in six.iteritems(values):
1196        if k in items:
1197          v = self.onduplicatekey_handler(self, k, items[k], v)
1198        items[k] = v
1199    else:
1200      # Get the existing arg value (if any)
1201      items = copy.copy(self._EnsureValue(namespace, self.dest, []))
1202      # Merge the new key/value pair(s) in
1203      for k in values:
1204        if k in items:
1205          self.onduplicatekey_handler(self, k)
1206        else:
1207          items.append(k)
1208
1209    # Saved the merged dictionary
1210    setattr(namespace, self.dest, items)
1211
1212
1213class UpdateActionWithAppend(UpdateAction):
1214  """Create a single dict value from delimited or repeated flags.
1215
1216  This class provides a variant of UpdateAction, which allows for users to
1217  append, rather than reject, duplicate key values. For example, the user
1218  can specify:
1219
1220    --inputs k1=v1a --inputs k1=v1b --inputs k2=v2
1221
1222  and the result will be:
1223
1224     { 'k1': ['v1a', 'v1b'], 'k2': 'v2' }
1225  """
1226
1227  def OnDuplicateKeyAppend(self, key, existing_value=None, new_value=None):
1228    if existing_value is None:
1229      return key
1230    elif isinstance(existing_value, list):
1231      return existing_value + [new_value]
1232    else:
1233      return [existing_value, new_value]
1234
1235  def __init__(self,
1236               option_strings,
1237               dest,
1238               nargs=None,
1239               const=None,
1240               default=None,
1241               type=None,  # pylint:disable=redefined-builtin
1242               choices=None,
1243               required=False,
1244               help=None,  # pylint:disable=redefined-builtin
1245               metavar=None,
1246               onduplicatekey_handler=OnDuplicateKeyAppend):
1247    super(UpdateActionWithAppend, self).__init__(
1248        option_strings=option_strings,
1249        dest=dest,
1250        nargs=nargs,
1251        const=const,
1252        default=default,
1253        type=type,
1254        choices=choices,
1255        required=required,
1256        help=help,
1257        metavar=metavar,
1258        onduplicatekey_handler=onduplicatekey_handler)
1259
1260
1261class RemainderAction(argparse._StoreAction):  # pylint: disable=protected-access
1262  """An action with a couple of helpers to better handle --.
1263
1264  argparse on its own does not properly handle -- implementation args.
1265  argparse.REMAINDER greedily steals valid flags before a --, and nargs='*' will
1266  bind to [] and not  parse args after --. This Action represents arguments to
1267  be passed through to a subcommand after --.
1268
1269  Primarily, this Action provides two utility parsers to help a modified
1270  ArgumentParser parse -- properly.
1271
1272  There is one additional property kwarg:
1273    example: A usage statement used to construct nice additional help.
1274  """
1275
1276  def __init__(self, *args, **kwargs):
1277    if kwargs['nargs'] is not argparse.REMAINDER:
1278      raise ValueError(
1279          'The RemainderAction should only be used when '
1280          'nargs=argparse.REMAINDER.')
1281
1282    # Create detailed help.
1283    self.explanation = (
1284        "The '--' argument must be specified between gcloud specific args on "
1285        'the left and {metavar} on the right.'
1286    ).format(metavar=kwargs['metavar'])
1287    if 'help' in kwargs:
1288      kwargs['help'] += '\n+\n' + self.explanation
1289      if 'example' in kwargs:
1290        kwargs['help'] += ' Example:\n\n' + kwargs['example']
1291        del kwargs['example']
1292    super(RemainderAction, self).__init__(*args, **kwargs)
1293
1294  def _SplitOnDash(self, args):
1295    split_index = args.index('--')
1296    # Remove -- before passing through
1297    return args[:split_index], args[split_index + 1:]
1298
1299  def ParseKnownArgs(self, args, namespace):
1300    """Binds all args after -- to the namespace."""
1301    # Not [], so that we can distinguish between empty remainder args and
1302    # absent remainder args.
1303    remainder_args = None
1304    if '--' in args:
1305      args, remainder_args = self._SplitOnDash(args)
1306    self(None, namespace, remainder_args)
1307    return namespace, args
1308
1309  def ParseRemainingArgs(self, remaining_args, namespace, original_args):
1310    """Parses the unrecognized args from the end of the remaining_args.
1311
1312    This method identifies all unrecognized arguments after the last argument
1313    recognized by a parser (but before --). It then either logs a warning and
1314    binds them to the namespace or raises an error, depending on strictness.
1315
1316    Args:
1317      remaining_args: A list of arguments that the parsers did not recognize.
1318      namespace: The Namespace to bind to.
1319      original_args: The full list of arguments given to the top parser,
1320
1321    Raises:
1322      ArgumentError: If there were remaining arguments after the last recognized
1323      argument and this action is strict.
1324
1325    Returns:
1326      A tuple of the updated namespace and unrecognized arguments (before the
1327      last recognized argument).
1328    """
1329    # Only parse consecutive unknown args from the end of the original args.
1330    # Strip out everything after '--'
1331    if '--' in original_args:
1332      original_args, _ = self._SplitOnDash(original_args)
1333    # Find common suffix between remaining_args and original_args
1334    split_index = 0
1335    for i, (arg1, arg2) in enumerate(
1336        zip(reversed(remaining_args), reversed(original_args))):
1337      if arg1 != arg2:
1338        split_index = len(remaining_args) - i
1339        break
1340    pass_through_args = remaining_args[split_index:]
1341    remaining_args = remaining_args[:split_index]
1342
1343    if pass_through_args:
1344      msg = ('unrecognized args: {args}\n' + self.explanation).format(
1345          args=' '.join(pass_through_args))
1346      raise parser_errors.UnrecognizedArgumentsError(msg)
1347    self(None, namespace, pass_through_args)
1348    return namespace, remaining_args
1349
1350
1351class StoreOnceAction(argparse.Action):
1352  r"""Create a single dict value from delimited flags.
1353
1354  For example, with the following flag definition:
1355
1356      parser.add_argument(
1357        '--inputs',
1358        type=arg_parsers.ArgDict(),
1359        action=StoreOnceAction)
1360
1361  a caller can specify on the command line flags such as:
1362
1363    --inputs k1=v1,k2=v2
1364
1365  and the result will be a list of one dict:
1366
1367    [{ 'k1': 'v1', 'k2': 'v2' }]
1368
1369  Specifying two separate command line flags such as:
1370
1371    --inputs k1=v1 \
1372    --inputs k2=v2
1373
1374  will raise an exception.
1375
1376  Note that this class will raise an exception if a key value is specified
1377  more than once. To allow for a key value to be specified multiple times,
1378  use UpdateActionWithAppend.
1379  """
1380
1381  def OnSecondArgumentRaiseError(self):
1382    raise argparse.ArgumentError(self, _GenerateErrorMessage(
1383        '"{0}" argument cannot be specified multiple times'.format(self.dest)))
1384
1385  def __init__(self, *args, **kwargs):
1386    self.dest_is_populated = False
1387    super(StoreOnceAction, self).__init__(*args, **kwargs)
1388
1389  # pylint: disable=protected-access
1390  def __call__(self, parser, namespace, values, option_string=None):
1391    # Make sure no existing arg value exist
1392    if self.dest_is_populated:
1393      self.OnSecondArgumentRaiseError()
1394    self.dest_is_populated = True
1395    setattr(namespace, self.dest, values)
1396
1397
1398def StoreOnceWarningAction(flag_name):
1399  """Emits a warning message when a flag is specified more than once.
1400
1401  The created action is similar to StoreOnceAction. The difference is that
1402  this action prints a warning message instead of raising an exception when the
1403  flag is specified more than once. Because it is a breaking change to switch an
1404  existing flag to StoreOnceAction, StoreOnceWarningAction can be used in the
1405  deprecation period.
1406
1407  Args:
1408    flag_name: The name of the flag to apply this action on.
1409
1410  Returns:
1411    An Action class.
1412  """
1413
1414  class Action(argparse.Action):
1415    """Emits a warning message when a flag is specified more than once."""
1416
1417    def OnSecondArgumentPrintWarning(self):
1418      log.warning(
1419          '"{0}" argument is specified multiple times which will be disallowed '
1420          'in future versions. Please only specify it once.'.format(flag_name))
1421
1422    def __init__(self, *args, **kwargs):
1423      self.dest_is_populated = False
1424      super(Action, self).__init__(*args, **kwargs)
1425
1426    def __call__(self, parser, namespace, values, option_string=None):
1427      # Make sure no existing arg value exist
1428      if self.dest_is_populated:
1429        self.OnSecondArgumentPrintWarning()
1430      self.dest_is_populated = True
1431      setattr(namespace, self.dest, values)
1432
1433  return Action
1434
1435
1436class _HandleNoArgAction(argparse.Action):
1437  """This class should not be used directly, use HandleNoArgAction instead."""
1438
1439  def __init__(self, none_arg, deprecation_message, **kwargs):
1440    super(_HandleNoArgAction, self).__init__(**kwargs)
1441    self.none_arg = none_arg
1442    self.deprecation_message = deprecation_message
1443
1444  def __call__(self, parser, namespace, value, option_string=None):
1445    if value is None:
1446      log.warning(self.deprecation_message)
1447      if self.none_arg:
1448        setattr(namespace, self.none_arg, True)
1449
1450    setattr(namespace, self.dest, value)
1451
1452
1453def HandleNoArgAction(none_arg, deprecation_message):
1454  """Creates an argparse.Action that warns when called with no arguments.
1455
1456  This function creates an argparse action which can be used to gracefully
1457  deprecate a flag using nargs=?. When a flag is created with this action, it
1458  simply log.warning()s the given deprecation_message and then sets the value of
1459  the none_arg to True.
1460
1461  This means if you use the none_arg no_foo and attach this action to foo,
1462  `--foo` (no argument), it will have the same effect as `--no-foo`.
1463
1464  Args:
1465    none_arg: a boolean argument to write to. For --no-foo use "no_foo"
1466    deprecation_message: msg to tell user to stop using with no arguments.
1467
1468  Returns:
1469    An argparse action.
1470
1471  """
1472  def HandleNoArgActionInit(**kwargs):
1473    return _HandleNoArgAction(none_arg, deprecation_message, **kwargs)
1474
1475  return HandleNoArgActionInit
1476
1477
1478class FileContents(object):
1479  """Creates an argparse type that reads the contents of a file or stdin.
1480
1481  This is similar to argparse.FileType, but unlike FileType it does not leave
1482  a dangling file handle open. The argument stored in the argparse Namespace
1483  is the file's contents.
1484
1485  Attributes:
1486    binary: bool, If True, the contents of the file will be returned as bytes.
1487
1488  Returns:
1489    A function that accepts a filename, or "-" representing that stdin should be
1490    used as input.
1491  """
1492
1493  def __init__(self, binary=False):
1494    self.binary = binary
1495
1496  def __call__(self, name):
1497    """Return the contents of the file with the specified name.
1498
1499    If name is "-", stdin is read until EOF. Otherwise, the named file is read.
1500
1501    Args:
1502      name: str, The file name, or '-' to indicate stdin.
1503
1504    Returns:
1505      The contents of the file.
1506
1507    Raises:
1508      ArgumentTypeError: If the file cannot be read or is too large.
1509    """
1510    try:
1511      return console_io.ReadFromFileOrStdin(name, binary=self.binary)
1512    except files.Error as e:
1513      raise ArgumentTypeError(e)
1514
1515
1516class YAMLFileContents(object):
1517  """Creates an argparse type that reads the contents of a YAML or JSON file.
1518
1519  This is similar to argparse.FileType, but unlike FileType it does not leave
1520  a dangling file handle open. The argument stored in the argparse Namespace
1521  is the file's contents parsed as a YAML object.
1522
1523  Attributes:
1524    validator: function, Function that will validate the provided input
1525    file contents.
1526
1527  Returns:
1528    A function that accepts a filename that should be parsed as a YAML
1529    or JSON file.
1530  """
1531
1532  def __init__(self, validator=None):
1533    if validator and not callable(validator):
1534      raise ArgumentTypeError('Validator must be callable')
1535    self.validator = validator
1536
1537  def _AssertJsonLike(self, yaml_data):
1538    if not (yaml.dict_like(yaml_data) or yaml.list_like(yaml_data)):
1539      raise ArgumentTypeError('Invalid YAML/JSON Data [{}]'.format(yaml_data))
1540
1541  def _LoadSingleYamlDocument(self, name):
1542    """Returns the yaml data for a file or from stdin for a single document.
1543
1544    YAML allows multiple documents in a single file by using `---` as a
1545    separator between documents. See https://yaml.org/spec/1.1/#id857577.
1546    However, some YAML-generating tools generate a single document followed by
1547    this separator before ending the file.
1548
1549    This method supports the case of a single document in a file that contains
1550    superfluous document separators, but still throws if multiple documents are
1551    actually found.
1552
1553    Args:
1554      name: str, The file path to the file or "-" to read from stdin.
1555
1556    Returns:
1557      The contents of the file parsed as a YAML data object.
1558    """
1559    if name == '-':
1560      stdin = console_io.ReadStdin()  # Save to potentially reuse below
1561      yaml_data = yaml.load_all(stdin)
1562    else:
1563      yaml_data = yaml.load_all_path(name)
1564    yaml_data = [d for d in yaml_data if d is not None]  # Remove empty docs
1565
1566    # Return the single document if only 1 is found.
1567    if len(yaml_data) == 1:
1568      return yaml_data[0]
1569
1570    # Multiple (or 0) documents found. Try to parse again with single-document
1571    # loader so its error is propagated rather than creating our own.
1572    if name == '-':
1573      return yaml.load(stdin)
1574    else:
1575      return yaml.load_path(name)
1576
1577  def __call__(self, name):
1578    """Load YAML data from file path (name) or stdin.
1579
1580    If name is "-", stdin is read until EOF. Otherwise, the named file is read.
1581    If self.validator is set, call it on the yaml data once it is loaded.
1582
1583    Args:
1584      name: str, The file path to the file.
1585
1586    Returns:
1587      The contents of the file parsed as a YAML data object.
1588
1589    Raises:
1590      ArgumentTypeError: If the file cannot be read or is not a JSON/YAML like
1591        object.
1592      ValueError: If file content fails validation.
1593    """
1594    try:
1595      yaml_data = self._LoadSingleYamlDocument(name)
1596      self._AssertJsonLike(yaml_data)
1597      if self.validator:
1598        if not self.validator(yaml_data):
1599          raise ValueError('Invalid YAML/JSON content [{}]'.format(yaml_data))
1600
1601      return yaml_data
1602
1603    except (yaml.YAMLParseError, yaml.FileLoadError) as e:
1604      raise ArgumentTypeError(e)
1605
1606
1607class StoreTrueFalseAction(argparse._StoreTrueAction):  # pylint: disable=protected-access
1608  """Argparse action that acts as a combination of store_true and store_false.
1609
1610  Calliope already gives any bool-type arguments the standard and `--no-`
1611  variants. In most cases we only want to document the option that does
1612  something---if we have `default=False`, we don't want to show `--no-foo`,
1613  since it won't do anything.
1614
1615  But in some cases we *do* want to show both variants: one example is when
1616  `--foo` means "enable," `--no-foo` means "disable," and neither means "do
1617  nothing." The obvious way to represent this is `default=None`; however, (1)
1618  the default value of `default` is already None, so most boolean actions would
1619  have this setting by default (not what we want), and (2) we still want an
1620  option to have this True/False/None behavior *without* the flag documentation.
1621
1622  To get around this, we have an opt-in version of the same thing that documents
1623  both the flag and its inverse.
1624  """
1625
1626  def __init__(self, *args, **kwargs):
1627    super(StoreTrueFalseAction, self).__init__(*args, default=None, **kwargs)
1628
1629
1630def StoreFilePathAndContentsAction(binary=False):
1631  """Returns Action that stores both file content and file path.
1632
1633  Args:
1634   binary: boolean, whether or not this is a binary file.
1635
1636  Returns:
1637   An argparse action.
1638  """
1639
1640  class Action(argparse.Action):
1641    """Stores both file content and file path.
1642
1643      Stores file contents under original flag DEST and stores file path under
1644      DEST_path.
1645    """
1646
1647    def __init__(self, *args, **kwargs):
1648      super(Action, self).__init__(*args, **kwargs)
1649
1650    def __call__(self, parser, namespace, value, option_string=None):
1651      """Stores the contents of the file and the file name in namespace."""
1652      try:
1653        content = console_io.ReadFromFileOrStdin(value, binary=binary)
1654      except files.Error as e:
1655        raise ArgumentTypeError(e)
1656      setattr(namespace, self.dest, content)
1657      new_dest = '{}_path'.format(self.dest)
1658      setattr(namespace, new_dest, value)
1659
1660  return Action
1661