#!/usr/bin/env python3
#
# formatters.py
"""
Formatters and syntax checkers.
"""
#
# Copyright © 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 json
import os
from configparser import ConfigParser
from io import StringIO
from typing import Any, List, NamedTuple, Optional
# 3rd party
import dom_toml
from domdf_python_tools.stringlist import StringList
import formate
from domdf_python_tools.paths import PathPlus
__all__ = (
"Formatter",
"format_toml",
"format_ini",
"format_json",
"format_python",
"noformat",
)
# 3rd party
from formate.config import get_hooks_for_filetype, parse_hooks
from typing_extensions import Protocol
class StringReformatter(formate.Reformatter):
def __init__(self, code: str, config: formate.FormateConfigDict, sort_imports: bool = True):
self.file_to_format = PathPlus(os.devnull) # in case someone tries to write to the file
self.filename = "snippet.py"
self.filetype = ".py"
self.config = config
self._unformatted_source = code
self._reformatted_source: Optional[str] = None
self.sort_imports = sort_imports
def to_file(self) -> None: # pragma: no cover
"""
Write the reformatted source to the original file.
"""
raise NotImplementedError(f"Unsupported by {self.__class__!r}")
def run(self) -> bool:
"""
Run the reformatter.
:return: Whether the file was changed.
"""
hooks = parse_hooks(self.config)
hooks = get_hooks_for_filetype(self.filetype, hooks)
isort_comment = '#' if self.sort_imports else "# isort: skip_file"
unformatted_source = f"{isort_comment}\n" + self._unformatted_source
reformatted_source = StringList(formate.call_hooks(hooks, unformatted_source, self.filename))
reformatted_source.blankline(ensure_single=True)
assert reformatted_source.pop(0) == isort_comment
self._reformatted_source = str(reformatted_source)
return self._reformatted_source != self._unformatted_source
class _ConsoleBlock(NamedTuple):
is_code: bool
lines: List[str]
def as_python(self) -> str:
return '\n'.join(self.lines)
def as_pycon(self) -> str:
if self.is_code:
buf = [f">>> {self.lines[0]}"]
buf.extend(f"... {line}" for line in self.lines[1:])
return '\n'.join(buf)
else:
return '\n'.join(self.lines)
def _format_pycon(code_lines: List[str], **config) -> str:
r"""
Check the syntax of, and reformat, the given Python console snippet.
:param code_lines: The code to check and reformat.
:param \*\*config: The language-specific configuration.
:returns: The reformatted code.
"""
# Split into blocks of code (>>> and any subsequent ...s) and outputs (unprefixed)
blocks: List[_ConsoleBlock] = []
for line in code_lines:
if line.startswith(">>> "):
blocks.append(_ConsoleBlock(True, [line[4:]]))
elif line.startswith("... "):
blocks[-1].lines.append(line[4:])
else:
if blocks[-1].is_code:
blocks.append(_ConsoleBlock(False, [line]))
else:
blocks[-1].lines.append(line)
reformatted_blocks: List[_ConsoleBlock] = []
for block in blocks:
if block.is_code:
config = {**config, "sort_imports": False}
reformatted_code = format_python(block.as_python(), **config)
reformatted_blocks.append(_ConsoleBlock(True, reformatted_code.splitlines()))
else:
reformatted_blocks.append(block)
return '\n'.join(block.as_pycon() for block in reformatted_blocks)