# -*- coding: utf-8 -*- # # Copyright 2023 Google LLC. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Utilities for adding help text for flags with an argparser type.""" from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals import abc from typing import Union import six class ArgTypeUsage(six.with_metaclass(abc.ABCMeta, object)): """Interface for flags types that need to provide additional usage info.""" @property @abc.abstractmethod def hidden(self): """Whether the argument is hidden.""" @abc.abstractmethod def GetUsageMetavar(self, is_custom_metavar, metavar): """Returns the metavar for flag with type self.""" @abc.abstractmethod def GetUsageExample(self, shorthand): """Returns the example user input value for flag with type self.""" @abc.abstractmethod def GetUsageHelpText(self, field_name, required, flag_name): """Returns the help text for flag with type self.""" class DefaultArgTypeWrapper(ArgTypeUsage): """Base class for processing arg_type output but maintaining usage help text. Attributes: arg_type: type function used to parse input string into correct type ie ArgObject(value_type=int, repeating=true), int, bool, etc """ def __init__(self, arg_type): super(DefaultArgTypeWrapper, self).__init__() self.arg_type = arg_type @property def _is_usage_type(self): return isinstance(self.arg_type, ArgTypeUsage) @property def hidden(self): if self._is_usage_type: return self.arg_type.hidden else: return None def GetUsageMetavar(self, *args, **kwargs): """Forwards default usage metavar for arg_type.""" if self._is_usage_type: return self.arg_type.GetUsageMetavar(*args, **kwargs) else: return None def GetUsageExample(self, *args, **kwargs): """Forwards default usage example for arg_type.""" if self._is_usage_type: return self.arg_type.GetUsageExample(*args, **kwargs) else: return None def GetUsageHelpText(self, *args, **kwargs): """Forwards default help text for arg_type.""" if self._is_usage_type: return self.arg_type.GetUsageHelpText(*args, **kwargs) else: return None def IsHidden(arg_type): """Returns whether arg_type is hidden. Args: arg_type: Callable, arg type that may contain hidden attribute Returns: bool, whether the type is considered hidden """ return (isinstance(arg_type, ArgTypeUsage) and arg_type.hidden) or False ASCII_INDENT = '::\n' def IndentAsciiDoc(text, depth=0): """Tabs over all lines in text using ascii doc syntax.""" additional_tabs = ':' * depth return text.replace(ASCII_INDENT, additional_tabs + ASCII_INDENT) def _FormatBasicTypeStr(arg_type): """Returns a user friendly name of a primitive arg_type. Args: arg_type: type | str | None, expected user input type Returns: String representation of the type """ if not arg_type: return None if isinstance(arg_type, str): # If arg_type is a string literal, return string literal return arg_type # Return string representation of common built in callable types if arg_type is int: return 'int' if arg_type is float: return 'float' if arg_type is bool: return 'boolean' # Default to string # TODO(b/296409952) Add common complex types such as enum or resource args return 'string' def _Punctuate(text): """Adds punctuation to text if it doesn't already exist.""" clean_text = text.rstrip() if clean_text.endswith('.'): return clean_text else: return clean_text + '.' def _Capitalize(text: str, ignore: bool = False) -> Union[str, None]: """Capitalizes the first letter of text. Args: text: The text to capitalize. ignore: Whether to ignore the capitalization request. Returns: The capitalized text. """ if not text or ignore: return text return text[0].upper() + text[1:] def FormatHelpText(field_name, required, help_text=None): """Defaults and formats specific attribute of help text. Args: field_name: None | str, attribute that is being set by flag required: bool, whether the flag is required help_text: None | str, text that describes the flag Returns: help text formatted as `{type} {required}, {help}` """ if help_text: defaulted_help_text = _Punctuate(help_text) elif field_name: defaulted_help_text = _Capitalize( 'sets `{}` value.'.format(field_name), required ) else: defaulted_help_text = _Capitalize('sets value.', required) if required: return 'Required, {}'.format(defaulted_help_text) else: return defaulted_help_text def FormatCodeSnippet(arg_name, arg_value, append=False): """Formats flag in markdown code snippet. Args: arg_name: str, name of the flag in snippet arg_value: str, flag value in snippet append: bool, whether to use append syntax for flag Returns: markdown string of example user input """ if ' ' in arg_value: example_flag = "{}='{}'".format(arg_name, arg_value) else: example_flag = '{}={}'.format(arg_name, arg_value) if append: return '```\n\n{input} {input}\n\n```'.format(input=example_flag) else: return '```\n\n{}\n\n```'.format(example_flag) def _GetNestedValueExample(arg_type, shorthand): """Gets an example input value for flag of arg_type. Args: arg_type: Callable[[str], Any] | str | None, expected user input type shorthand: bool, whether to display example in shorthand Returns: string representation of user input for type arg_type """ if not arg_type: return None if isinstance(arg_type, ArgTypeUsage): arg_str = arg_type.GetUsageExample(shorthand=shorthand) else: arg_str = _FormatBasicTypeStr(arg_type) is_string_literal = isinstance(arg_type, str) is_string_type = arg_str == _FormatBasicTypeStr(str) if not shorthand and (is_string_literal or is_string_type): return '"{}"'.format(arg_str) else: return arg_str def GetNestedKeyValueExample(key_type, value_type, shorthand): """Formats example key-value input for flag of arg_type. If key_type and value_type are callable types str, returns string=string (shorthand) or "string": "string" (non-shorthand) If key_type is a static string value such as x, returns x=string (shorthand) or "x": "string" (non-shorthand). If key_type or value_type are None, returns string representation of key or value Args: key_type: Callable[[str], Any] | str | None, type function for the key value_type: Callable[[str], Any] | None, type function for the value shorthand: bool, whether to display the example in shorthand Returns: str, example of key-value pair """ key_str = _GetNestedValueExample(key_type, shorthand) value_str = _GetNestedValueExample(value_type, shorthand) if IsHidden(key_type) or IsHidden(value_type): return None elif not key_str or not value_str: return key_str or value_str elif shorthand: return f'{key_str}={value_str}' if value_str != '{}' else key_str else: return f'{key_str}: {value_str}' def GetNestedUsageHelpText(field_name, arg_type, required=False): """Returns help text for flag with arg_type. Generates help text based on schema such that the final output will look something like... *Foo* Required, Foo help text Args: field_name: str, attribute we are generating help text for arg_type: Callable[[str], Any] | None, type of the attribute we are getting help text for required: bool, whether the attribute is required Returns: string help text for specific attribute """ default_usage = FormatHelpText(field_name, required) if isinstance(arg_type, ArgTypeUsage) and arg_type.hidden: usage = None elif isinstance(arg_type, ArgTypeUsage) and not arg_type.hidden: usage = ( arg_type.GetUsageHelpText(field_name, required=required) or default_usage ) else: usage = default_usage # Shift (indent) nested content over to the right by one if usage: return '*{}*{}{}'.format( field_name, ASCII_INDENT, IndentAsciiDoc(usage, depth=1) ) else: return None