|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
"""Internal helper functions for Abseil Python flags library.""" |
|
|
|
import os |
|
import re |
|
import struct |
|
import sys |
|
import textwrap |
|
import types |
|
from typing import Any, Dict, Iterable, List, NamedTuple, Optional, Sequence, Set |
|
from xml.dom import minidom |
|
|
|
try: |
|
import fcntl |
|
except ImportError: |
|
fcntl = None |
|
try: |
|
|
|
import termios |
|
except ImportError: |
|
termios = None |
|
|
|
|
|
|
|
_DEFAULT_HELP_WIDTH = 80 |
|
|
|
|
|
_MIN_HELP_WIDTH = 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_SUGGESTION_ERROR_RATE_THRESHOLD = 0.50 |
|
|
|
|
|
|
|
|
|
_ILLEGAL_XML_CHARS_REGEX = re.compile( |
|
u'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x84\x86-\x9f\ud800-\udfff\ufffe\uffff]') |
|
|
|
|
|
|
|
|
|
disclaim_module_ids: Set[int] = set([id(sys.modules[__name__])]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
SPECIAL_FLAGS: Any = None |
|
|
|
|
|
|
|
|
|
|
|
FLAGS_MODULE: types.ModuleType = None |
|
|
|
|
|
class _ModuleObjectAndName(NamedTuple): |
|
"""Module object and name. |
|
|
|
Fields: |
|
- module: object, module object. |
|
- module_name: str, module name. |
|
""" |
|
module: types.ModuleType |
|
module_name: str |
|
|
|
|
|
def get_module_object_and_name( |
|
globals_dict: Dict[str, Any] |
|
) -> _ModuleObjectAndName: |
|
"""Returns the module that defines a global environment, and its name. |
|
|
|
Args: |
|
globals_dict: A dictionary that should correspond to an environment |
|
providing the values of the globals. |
|
|
|
Returns: |
|
_ModuleObjectAndName - pair of module object & module name. |
|
Returns (None, None) if the module could not be identified. |
|
""" |
|
name = globals_dict.get('__name__', None) |
|
module = sys.modules.get(name, None) |
|
|
|
return _ModuleObjectAndName(module, |
|
(sys.argv[0] if name == '__main__' else name)) |
|
|
|
|
|
def get_calling_module_object_and_name() -> _ModuleObjectAndName: |
|
"""Returns the module that's calling into this module. |
|
|
|
We generally use this function to get the name of the module calling a |
|
DEFINE_foo... function. |
|
|
|
Returns: |
|
The module object that called into this one. |
|
|
|
Raises: |
|
AssertionError: Raised when no calling module could be identified. |
|
""" |
|
for depth in range(1, sys.getrecursionlimit()): |
|
|
|
|
|
globals_for_frame = sys._getframe(depth).f_globals |
|
module, module_name = get_module_object_and_name(globals_for_frame) |
|
if id(module) not in disclaim_module_ids and module_name is not None: |
|
return _ModuleObjectAndName(module, module_name) |
|
raise AssertionError('No module was found') |
|
|
|
|
|
def get_calling_module() -> str: |
|
"""Returns the name of the module that's calling into this module.""" |
|
return get_calling_module_object_and_name().module_name |
|
|
|
|
|
def create_xml_dom_element( |
|
doc: minidom.Document, name: str, value: Any |
|
) -> minidom.Element: |
|
"""Returns an XML DOM element with name and text value. |
|
|
|
Args: |
|
doc: minidom.Document, the DOM document it should create nodes from. |
|
name: str, the tag of XML element. |
|
value: object, whose string representation will be used |
|
as the value of the XML element. Illegal or highly discouraged xml 1.0 |
|
characters are stripped. |
|
|
|
Returns: |
|
An instance of minidom.Element. |
|
""" |
|
s = str(value) |
|
if isinstance(value, bool): |
|
|
|
s = s.lower() |
|
|
|
s = _ILLEGAL_XML_CHARS_REGEX.sub(u'', s) |
|
|
|
e = doc.createElement(name) |
|
e.appendChild(doc.createTextNode(s)) |
|
return e |
|
|
|
|
|
def get_help_width() -> int: |
|
"""Returns the integer width of help lines that is used in TextWrap.""" |
|
if not sys.stdout.isatty() or termios is None or fcntl is None: |
|
return _DEFAULT_HELP_WIDTH |
|
try: |
|
data = fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, b'1234') |
|
columns = struct.unpack('hh', data)[1] |
|
|
|
|
|
if columns >= _MIN_HELP_WIDTH: |
|
return columns |
|
|
|
return int(os.getenv('COLUMNS', _DEFAULT_HELP_WIDTH)) |
|
|
|
except (TypeError, IOError, struct.error): |
|
return _DEFAULT_HELP_WIDTH |
|
|
|
|
|
def get_flag_suggestions( |
|
attempt: Optional[str], longopt_list: Sequence[str] |
|
) -> List[str]: |
|
"""Returns helpful similar matches for an invalid flag.""" |
|
|
|
if len(attempt) <= 2 or not longopt_list: |
|
return [] |
|
|
|
option_names = [v.split('=')[0] for v in longopt_list] |
|
|
|
|
|
|
|
distances = [(_damerau_levenshtein(attempt, option[0:len(attempt)]), option) |
|
for option in option_names] |
|
|
|
distances.sort() |
|
|
|
least_errors, _ = distances[0] |
|
|
|
if least_errors >= _SUGGESTION_ERROR_RATE_THRESHOLD * len(attempt): |
|
return [] |
|
|
|
suggestions = [] |
|
for errors, name in distances: |
|
if errors == least_errors: |
|
suggestions.append(name) |
|
else: |
|
break |
|
return suggestions |
|
|
|
|
|
def _damerau_levenshtein(a, b): |
|
"""Returns Damerau-Levenshtein edit distance from a to b.""" |
|
memo = {} |
|
|
|
def distance(x, y): |
|
"""Recursively defined string distance with memoization.""" |
|
if (x, y) in memo: |
|
return memo[x, y] |
|
if not x: |
|
d = len(y) |
|
elif not y: |
|
d = len(x) |
|
else: |
|
d = min( |
|
distance(x[1:], y) + 1, |
|
distance(x, y[1:]) + 1, |
|
distance(x[1:], y[1:]) + (x[0] != y[0])) |
|
if len(x) >= 2 and len(y) >= 2 and x[0] == y[1] and x[1] == y[0]: |
|
|
|
t = distance(x[2:], y[2:]) + 1 |
|
if d > t: |
|
d = t |
|
|
|
memo[x, y] = d |
|
return d |
|
return distance(a, b) |
|
|
|
|
|
def text_wrap( |
|
text: str, |
|
length: Optional[int] = None, |
|
indent: str = '', |
|
firstline_indent: Optional[str] = None, |
|
) -> str: |
|
"""Wraps a given text to a maximum line length and returns it. |
|
|
|
It turns lines that only contain whitespace into empty lines, keeps new lines, |
|
and expands tabs using 4 spaces. |
|
|
|
Args: |
|
text: str, text to wrap. |
|
length: int, maximum length of a line, includes indentation. |
|
If this is None then use get_help_width() |
|
indent: str, indent for all but first line. |
|
firstline_indent: str, indent for first line; if None, fall back to indent. |
|
|
|
Returns: |
|
str, the wrapped text. |
|
|
|
Raises: |
|
ValueError: Raised if indent or firstline_indent not shorter than length. |
|
""" |
|
|
|
if length is None: |
|
length = get_help_width() |
|
if indent is None: |
|
indent = '' |
|
if firstline_indent is None: |
|
firstline_indent = indent |
|
|
|
if len(indent) >= length: |
|
raise ValueError('Length of indent exceeds length') |
|
if len(firstline_indent) >= length: |
|
raise ValueError('Length of first line indent exceeds length') |
|
|
|
text = text.expandtabs(4) |
|
|
|
result = [] |
|
|
|
|
|
wrapper = textwrap.TextWrapper( |
|
width=length, initial_indent=firstline_indent, subsequent_indent=indent) |
|
subsequent_wrapper = textwrap.TextWrapper( |
|
width=length, initial_indent=indent, subsequent_indent=indent) |
|
|
|
|
|
|
|
|
|
|
|
for paragraph in (p.strip() for p in text.splitlines()): |
|
if paragraph: |
|
result.extend(wrapper.wrap(paragraph)) |
|
else: |
|
result.append('') |
|
|
|
wrapper = subsequent_wrapper |
|
|
|
return '\n'.join(result) |
|
|
|
|
|
def flag_dict_to_args( |
|
flag_map: Dict[str, Any], multi_flags: Optional[Set[str]] = None |
|
) -> Iterable[str]: |
|
"""Convert a dict of values into process call parameters. |
|
|
|
This method is used to convert a dictionary into a sequence of parameters |
|
for a binary that parses arguments using this module. |
|
|
|
Args: |
|
flag_map: dict, a mapping where the keys are flag names (strings). |
|
values are treated according to their type: |
|
|
|
* If value is ``None``, then only the name is emitted. |
|
* If value is ``True``, then only the name is emitted. |
|
* If value is ``False``, then only the name prepended with 'no' is |
|
emitted. |
|
* If value is a string then ``--name=value`` is emitted. |
|
* If value is a collection, this will emit |
|
``--name=value1,value2,value3``, unless the flag name is in |
|
``multi_flags``, in which case this will emit |
|
``--name=value1 --name=value2 --name=value3``. |
|
* Everything else is converted to string an passed as such. |
|
|
|
multi_flags: set, names (strings) of flags that should be treated as |
|
multi-flags. |
|
Yields: |
|
sequence of string suitable for a subprocess execution. |
|
""" |
|
for key, value in flag_map.items(): |
|
if value is None: |
|
yield '--%s' % key |
|
elif isinstance(value, bool): |
|
if value: |
|
yield '--%s' % key |
|
else: |
|
yield '--no%s' % key |
|
elif isinstance(value, (bytes, type(u''))): |
|
|
|
yield '--%s=%s' % (key, value) |
|
else: |
|
|
|
try: |
|
if multi_flags and key in multi_flags: |
|
for item in value: |
|
yield '--%s=%s' % (key, str(item)) |
|
else: |
|
yield '--%s=%s' % (key, ','.join(str(item) for item in value)) |
|
except TypeError: |
|
|
|
yield '--%s=%s' % (key, value) |
|
|
|
|
|
def trim_docstring(docstring: str) -> str: |
|
"""Removes indentation from triple-quoted strings. |
|
|
|
This is the function specified in PEP 257 to handle docstrings: |
|
https://www.python.org/dev/peps/pep-0257/. |
|
|
|
Args: |
|
docstring: str, a python docstring. |
|
|
|
Returns: |
|
str, docstring with indentation removed. |
|
""" |
|
if not docstring: |
|
return '' |
|
|
|
|
|
max_indent = 1 << 29 |
|
|
|
|
|
|
|
lines = docstring.expandtabs().splitlines() |
|
|
|
|
|
indent = max_indent |
|
for line in lines[1:]: |
|
stripped = line.lstrip() |
|
if stripped: |
|
indent = min(indent, len(line) - len(stripped)) |
|
|
|
trimmed = [lines[0].strip()] |
|
if indent < max_indent: |
|
for line in lines[1:]: |
|
trimmed.append(line[indent:].rstrip()) |
|
|
|
while trimmed and not trimmed[-1]: |
|
trimmed.pop() |
|
while trimmed and not trimmed[0]: |
|
trimmed.pop(0) |
|
|
|
return '\n'.join(trimmed) |
|
|
|
|
|
def doc_to_help(doc: str) -> str: |
|
"""Takes a __doc__ string and reformats it as help.""" |
|
|
|
|
|
|
|
|
|
doc = doc.strip() |
|
|
|
|
|
whitespace_only_line = re.compile('^[ \t]+$', re.M) |
|
doc = whitespace_only_line.sub('', doc) |
|
|
|
|
|
doc = trim_docstring(doc) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
doc = re.sub(r'(?<=\S)\n(?=\S)', ' ', doc, flags=re.M) |
|
|
|
return doc |
|
|