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