Source code for formate.mini_hooks

#!/usr/bin/env python3
#
#  mini_hooks.py
"""
Small but mighty hooks.
"""
#
#  Copyright © 2020-2021 Dominic Davis-Foster <dominic@davis-foster.co.uk>
#
#  Permission is hereby granted, free of charge, to any person obtaining a copy
#  of this software and associated documentation files (the "Software"), to deal
#  in the Software without restriction, including without limitation the rights
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#  copies of the Software, and to permit persons to whom the Software is
#  furnished to do so, subject to the following conditions:
#
#  The above copyright notice and this permission notice shall be included in all
#  copies or substantial portions of the Software.
#
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
#  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
#  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
#  OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
#  OR OTHER DEALINGS IN THE SOFTWARE.
#

# stdlib
import ast
import re
from typing import List

# 3rd party
from domdf_python_tools.paths import PathPlus
from domdf_python_tools.stringlist import StringList
from domdf_python_tools.typing import PathLike

# this package
from formate.config import wants_filename

__all__ = ("noqa_reformat", "check_ast", "squish_stubs")


[docs]def noqa_reformat(source: str) -> str: """ Pull ``# noqa: ...`` comments that immediately follow docstrings back up to the end of the correct line. :param source: The source to reformat. :return: The reformatted source. """ return re.sub(r'"""[\n\s]+#\s+noqa', '""" # noqa', source)
[docs]def check_ast(source: str) -> str: """ Check the source can be parsed as a Python Abstract Syntax Tree. :param source: The source to check. :raises SyntaxError: If the source is not valid Python. :return: The source unchanged. """ ast.parse(source) return source
[docs]@wants_filename def squish_stubs(source: str, formate_filename: PathLike) -> str: """ Squash type stubs by removing unnecessary blank lines. .. versionadded:: 0.2.0 :param source: The source to check. :param formate_filename: The name of the source file, to ensure this hook only runs on type stubs. :return: The reformatted source. """ filename = PathPlus(formate_filename) if filename.suffix != ".pyi": return source blocks = _breakup_source(source) return str(_reformat_blocks(blocks))
class _Variables(list): pass class _Class(list): pass class _Function(list): pass class _DecoratedFunction(_Function): pass class _Decorator(list): pass class _MultilineFunction(_Function): pass def _breakup_source(source: str) -> List[List[str]]: blocks: List[List[str]] = [[]] split_az_re = re.compile("[A-Za-z]") split_az_underscore_re = re.compile("[A-Za-z)*_]") for line in source.split('\n'): if not line.strip(): if isinstance(blocks[-1], _Variables): blocks[-1].append(line) elif line.lstrip().startswith('@'): if isinstance(blocks[-1], _Decorator): blocks[-1].append(line) else: blocks.append(_Decorator([line])) elif line.lstrip().startswith("def "): if isinstance(blocks[-1], _Decorator): blocks[-1] = _DecoratedFunction([*blocks[-1], line]) else: blocks.append(_Function([line])) elif line.lstrip().startswith("class "): # TODO: decorated classes? blocks.append(_Class([line])) elif line.rstrip().startswith(' ') or line.startswith('\t'): if isinstance(blocks[-1], _Class): blocks[-1].append(line) elif isinstance(blocks[-1], _MultilineFunction): if len(blocks[-1]) < 2: blocks[-1].append(line) elif split_az_underscore_re.split(line)[0] == split_az_underscore_re.split(blocks[-1][-1])[0]: blocks[-1].append(line) else: blocks.append(_Variables([line])) elif isinstance(blocks[-1], _Function): if split_az_re.split(line)[0] == split_az_re.split(blocks[-1][-1])[0]: blocks.append(_Variables([line])) elif line.rstrip().endswith(','): blocks[-1] = _MultilineFunction([*blocks[-1], line]) else: blocks.append(_Variables([line])) elif isinstance(blocks[-1], _Variables): blocks[-1].append(line) else: blocks.append(_Variables([line])) else: if isinstance(blocks[-1], _Variables): blocks[-1].append(line) else: blocks.append(_Variables([line])) return blocks def _reformat_blocks(blocks: List[List[str]]) -> StringList: cursor = 1 tab_re = re.compile("^[ \t]+") while cursor < len(blocks): # pylint: disable=loop-invariant-statement if isinstance(blocks[cursor - 1], (_MultilineFunction, _DecoratedFunction, _Class)): # Add a blank line after _Variables, a multi-line function, or a decorated function blocks.insert(cursor, []) cursor += 1 if ( blocks[cursor] and blocks[cursor - 1] and tab_re.match(blocks[cursor - 1][-1]) and not tab_re.match(blocks[cursor][0]) ): # Add a blank line after a dedent blocks.insert(cursor, []) cursor += 1 if isinstance(blocks[cursor - 1], _Variables): # Add a blank line before and after _Variables blocks.insert(cursor - 1, []) blocks.insert(cursor + 1, []) cursor += 2 if isinstance(blocks[cursor], _Variables): # Add a blank line before and after _Variables blocks.insert(cursor, []) blocks.insert(cursor + 2, []) cursor += 2 if isinstance(blocks[cursor], (_DecoratedFunction, _MultilineFunction)): # Add a blank line before a decorated function blocks.insert(cursor, []) cursor += 1 if isinstance(blocks[cursor], _Class): if ( cursor + 1 < len(blocks) and isinstance(blocks[cursor + 1], _Function) and not isinstance(blocks[cursor + 1], (_DecoratedFunction, _MultilineFunction)) and blocks[cursor][-1].lstrip().startswith("class") and blocks[cursor + 1][0][0].isspace() ): # pylint: enable=loop-invariant-statement blocks.insert(cursor, []) cursor += 2 else: blocks.insert(cursor, []) blocks.insert(cursor + 2, []) cursor += 3 cursor += 1 output = StringList() # Remove trailing whitespace from each block for block in blocks: if output and not block and not output[-1]: # Remove duplicate new lines continue output.append('\n'.join(block).rstrip()) if not output[0]: output.pop(0) output.blankline(ensure_single=True) return output