1# Copyright 2024 The Pigweed Authors 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); you may not 4# use this file except in compliance with the License. You may obtain a copy of 5# the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12# License for the specific language governing permissions and limitations under 13# the License. 14"""Code formatter plugins for Python.""" 15 16import os 17from pathlib import Path 18from typing import Iterable, Iterator, Optional, Sequence, Tuple, Union 19 20from pw_presubmit.format.core import ( 21 FileFormatter, 22 FormattedFileContents, 23 FormatFixStatus, 24) 25 26 27class BlackFormatter(FileFormatter): 28 """A formatter that runs ``black`` on files.""" 29 30 def __init__(self, config_file: Optional[Path], **kwargs): 31 super().__init__(**kwargs) 32 self.config_file = config_file 33 34 def _config_file_args(self) -> Sequence[Union[str, Path]]: 35 if self.config_file is not None: 36 return ('--config', Path(self.config_file)) 37 38 return () 39 40 def format_file_in_memory( 41 self, file_path: Path, file_contents: bytes 42 ) -> FormattedFileContents: 43 """Uses ``black`` to check the formatting of the requested file. 44 45 The file at ``file_path`` is NOT modified by this check. 46 47 Returns: 48 A populated 49 :py:class:`pw_presubmit.format.core.FormattedFileContents` that 50 contains either the result of formatting the file, or an error 51 message. 52 """ 53 proc = self.run_tool( 54 'black', 55 [*self._config_file_args(), '-q', '-'], 56 input=file_contents, 57 ) 58 ok = proc.returncode == 0 59 formatted_file_contents = proc.stdout if ok else b'' 60 61 # On Windows, Black's stdout always has CRLF line endings. 62 if os.name == 'nt': 63 formatted_file_contents = formatted_file_contents.replace( 64 b'\r\n', b'\n' 65 ) 66 67 return FormattedFileContents( 68 ok=ok, 69 formatted_file_contents=formatted_file_contents, 70 error_message=None if ok else proc.stderr.decode(), 71 ) 72 73 def format_file(self, file_path: Path) -> FormatFixStatus: 74 """Formats the provided file in-place using ``black``. 75 76 Returns: 77 A FormatFixStatus that contains relevant errors/warnings. 78 """ 79 proc = self.run_tool( 80 'black', 81 [*self._config_file_args(), '-q', file_path], 82 ) 83 ok = proc.returncode == 0 84 return FormatFixStatus( 85 ok=ok, 86 error_message=None if ok else proc.stderr.decode(), 87 ) 88 89 def format_files( 90 self, paths: Iterable[Path], keep_warnings: bool = True 91 ) -> Iterator[Tuple[Path, FormatFixStatus]]: 92 """Uses ``black`` to format the specified files in-place. 93 94 Returns: 95 An iterator of ``Path`` and 96 :py:class:`pw_presubmit.format.core.FormatFixStatus` pairs for each 97 file that was not successfully formatted. If ``keep_warnings`` is 98 ``True``, any successful format operations with warnings will also 99 be returned. 100 """ 101 proc = self.run_tool( 102 'black', 103 [*self._config_file_args(), '-q', *paths], 104 ) 105 106 # If there's an error, fall back to per-file formatting to figure out 107 # which file has problems. 108 if proc.returncode != 0: 109 yield from super().format_files(paths, keep_warnings) 110