314 lines
8.6 KiB
Python
314 lines
8.6 KiB
Python
# -*- 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
|