#!/usr/bin/env python3
#
# __init__.py
"""
Python formatting mate.
"""
#
# 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
from configparser import ConfigParser
from typing import Iterable, Mapping, Optional, Sequence
# 3rd party
import click
import isort
from consolekit.terminal_colours import ColourTrilean, resolve_color_default
from consolekit.utils import coloured_diff
from domdf_python_tools.paths import PathPlus, TemporaryPathPlus
from domdf_python_tools.stringlist import StringList
from domdf_python_tools.typing import PathLike
from domdf_python_tools.words import TAB
from isort.exceptions import FileSkipComment
# this package
from formate.classes import FormateConfigDict, Hook
from formate.config import parse_hooks, wants_filename, wants_global_config
from formate.utils import _find_from_parents, syntaxerror_for_file
__author__: str = "Dominic Davis-Foster"
__copyright__: str = "2020-2021 Dominic Davis-Foster"
__license__: str = "MIT License"
__version__: str = "0.7.0"
__email__: str = "dominic@davis-foster.co.uk"
__all__ = ("call_hooks", "reformat_file", "Reformatter", "isort_hook", "yapf_hook")
# TODO: Ideas for hooks
# * https://github.com/asottile/add-trailing-comma
# * Replace collections imports with their typing equivalents where possible.
# * nested with block reformat, dependent on linelength
# * replace typing imports with typing_extensions where necessary. Needs min version flag
# * replace e.g `import numpy as np` with `import numpy` and update all usages
# * replace wildcard import with the things what is imported
# * replace `exit()` with `sys.exit()` and add import if required
[docs]def call_hooks(hooks: Iterable[Hook], source: str, filename: PathLike) -> str:
"""
Given a list of hooks (in order), call them in turn to reformat the source.
:param hooks:
:param source: The source to reformat.
:param filename: The name of the source file.
:returns: The reformatted source.
.. versionchanged:: 0.4.3 Added the ``filename`` argument.
"""
for hook in hooks:
source = hook(source, filename)
return source
isort_string_or_sequence = {
"skip",
"skip_glob",
"sections",
"known_future_library",
"known_third_party",
"known_first_party",
"known_local_folder",
"known_standard_library",
"extra_standard_library",
"forced_separate",
"length_sort_sections",
"add_imports",
"remove_imports",
"single_line_exclusions",
"no_lines_before",
"sources",
"src_paths",
"treat_comments_as_code",
"supported_extensions",
"blocked_extensions",
"constants",
"classes",
"variables",
"namespace_packages",
}
# TODO: known_other, dict
[docs]@wants_filename
@wants_global_config
def isort_hook(
source: str,
formate_filename: PathLike,
formate_global_config: Optional[Mapping] = None,
**kwargs,
) -> str:
r"""
Call `isort <https://pypi.org/project/isort/>`_, using the given keyword arguments as its configuration.
:param source: The source to reformat.
:param formate_filename: The path to the file being reformatted.
:param formate_global_config: The global configuration dictionary. Optional.
:param \*\*kwargs:
:returns: The reformatted source.
"""
if "isort_config_file" in kwargs:
isort_config = isort.Config(settings_file=str(kwargs["isort_config_file"]))
else:
if "line_length" not in kwargs and formate_global_config:
if "line_length" in (formate_global_config or {}):
kwargs["line_length"] = formate_global_config["line_length"]
parsed_kwargs = {}
import_headings = {}
for option, value in kwargs.items():
if option.startswith("import_heading"):
import_headings[option[len("import_heading") + 1:]] = value
elif option in isort_string_or_sequence: # pylint: disable=loop-global-usage
if isinstance(value, str):
value = (value, )
elif not isinstance(value, Sequence):
value = (value, )
parsed_kwargs[option] = value
elif option == "force_to_top":
continue # TODO isort expects a frozenset but I thought it was boolean?
elif option == "remove_redundant_aliases":
continue
else:
parsed_kwargs[option] = value
isort_config = isort.Config(import_headings=import_headings, **parsed_kwargs)
if PathPlus(formate_filename).suffix == ".pyi":
object.__setattr__(isort_config, "remove_redundant_aliases", False)
try:
return isort.code(source, config=isort_config)
except FileSkipComment:
return source
[docs]@wants_global_config
def yapf_hook(source: str, formate_global_config: Optional[Mapping] = None, **kwargs) -> str:
r"""
Call `yapf <https://github.com/google/yapf>`_, using the given keyword arguments as its configuration.
:param source: The source to reformat.
:param formate_global_config: The global configuration dictionary. Optional.
:param \*\*kwargs:
If ``yapf_style`` is given as a keyword argument, use that style.
If a filename is given as the style it is searched for in the current and parent directories, and the style taken from the configuration in that file.
:returns: The reformatted source.
"""
# 3rd party
from yapf.yapflib.yapf_api import FormatCode # type: ignore[import]
if "yapf_style" in kwargs:
# yapf_style may be a filename or the name of a style
# If `yapf_style` is a filename (or the name of a style, as opposed to a path), look in CWD and parent directories
yapf_style = _find_from_parents(PathPlus(kwargs["yapf_style"]))
return FormatCode(source, style_config=str(yapf_style))[0]
else:
if "use_tabs" not in kwargs and formate_global_config:
if "indent" in (formate_global_config or {}):
kwargs["use_tabs"] = formate_global_config["indent"] == TAB
if "column_limit" not in kwargs and formate_global_config:
if "line_length" in (formate_global_config or {}):
kwargs["column_limit"] = formate_global_config["line_length"]
with TemporaryPathPlus() as tmpdir:
config_file = tmpdir / ".style.yapf"
config = ConfigParser()
config.read_dict({"style": kwargs})
with config_file.open('w') as fp:
config.write(fp)
return FormatCode(source, style_config=str(config_file))[0]