| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| """Internal helper functions for Abseil Python flags library.""" |
|
|
| from collections.abc import Iterable, Sequence |
| import re |
| import shutil |
| import sys |
| import textwrap |
| import types |
| from typing import Any, NamedTuple |
| from xml.dom import minidom |
|
|
|
|
| _DEFAULT_HELP_WIDTH = 80 |
| |
| |
| _MIN_HELP_WIDTH = 40 |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| _SUGGESTION_ERROR_RATE_THRESHOLD = 0.50 |
|
|
| |
| |
| |
| _ILLEGAL_XML_CHARS_REGEX = re.compile( |
| '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x84\x86-\x9f\ud800-\udfff\ufffe\uffff]' |
| ) |
|
|
| |
| |
| |
| disclaim_module_ids: set[int] = {id(sys.modules[__name__])} |
|
|
|
|
| |
| |
| |
| |
| |
| SPECIAL_FLAGS: Any = None |
|
|
|
|
| |
| |
| |
| FLAGS_MODULE: types.ModuleType | None = 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 | None: |
| """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 if the module could not be identified. |
| """ |
| try: |
| name = globals_dict['__name__'] |
| module = sys.modules[name] |
| except KeyError: |
| return 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 = get_module_object_and_name(globals_for_frame) |
| if module is not None and id(module.module) not in disclaim_module_ids: |
| return module |
| 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('', 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.""" |
| size = shutil.get_terminal_size(fallback=(_DEFAULT_HELP_WIDTH, 1)) |
| return size.columns |
|
|
|
|
| def get_flag_suggestions( |
| attempt: 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: int | None = None, |
| indent: str = '', |
| firstline_indent: str | None = 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: Text to wrap. |
| length: Maximum length of a line, includes indentation. If this is `None` |
| then use `get_help_width()`. |
| indent: Indent for all but first line. |
| firstline_indent: Indent for first line. If `None`, fall back to `indent`. |
| |
| Returns: |
| 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: set[str] | None = 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, str)): |
| |
| 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 |
|
|