blob: 56052285b0650d1c8ea713f36a7c70e8b1afb976 [file] [log] [blame]
# Copyright 2022 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Python formatter."""
import os
from pathlib import Path
from typing import Optional, Union
from chromite.format import formatters
from chromite.lib import constants
from chromite.lib import cros_build_lib
from chromite.lib import git
def _find_pyproject_toml(
path: Optional[Union[str, os.PathLike]] = None
) -> Path:
"""Locate pyproject.toml to use with |path|."""
if path:
project_root = git.FindGitTopLevel(path)
for dir_path in Path(path).resolve().parents:
config_path = dir_path / "pyproject.toml"
try:
if "[tool.black]" in config_path.read_text(encoding="utf-8"):
return config_path
except FileNotFoundError:
pass
if dir_path == project_root:
break
return constants.CHROMITE_DIR / "pyproject.toml"
def _custom_format_data(data: str) -> str:
"""Apply some custom rules that black doesn't handle."""
lines = data.splitlines()
def _trim_blank_comments(i):
"""Trim blank lines & empty comment lines at the top of the file."""
while len(lines) > i:
if lines[i] in ("", "#"):
lines.pop(i)
else:
break
# Trim leading comment lines.
_trim_blank_comments(0)
if lines and lines[0].startswith("#!"):
# Skip shebang and trim some more.
_trim_blank_comments(1)
# Skip license block and trim some more.
i = None
for i, line in enumerate(lines):
if line and not line.startswith("#"):
if line.startswith('"""'):
# Clean up the content around the module docstring.
while i and lines[i - 1] in ("", "#"):
i -= 1
_trim_blank_comments(i)
lines.insert(i, "")
break
return "\n".join(lines) + "\n" if lines else ""
def Data(
data: str,
path: Optional[Union[str, os.PathLike]] = None,
) -> str:
"""Format python |data|.
Args:
data: The file content to lint.
path: The file name for diagnostics/configs/etc...
Returns:
Formatted data.
"""
# We run through isort first to enforce module sorting order, then run that
# result through black. We can't run isort independently (or after black)
# because it has some known edge cases where it formats in
# black-incompatible ways. See b/235526476 for details.
result = cros_build_lib.run(
[
constants.CHROMITE_SCRIPTS_DIR / "isort",
"--settings-file",
constants.CHROMITE_DIR / ".isort.cfg",
"-",
"-d",
],
capture_output=True,
input=data,
encoding="utf-8",
)
try:
result = cros_build_lib.run(
[
constants.CHROMITE_SCRIPTS_DIR / "black",
f"--config={_find_pyproject_toml(path)}",
f"--stdin-filename={path}",
"-",
],
input=result.stdout,
capture_output=True,
encoding="utf-8",
)
except cros_build_lib.RunCommandError as e:
# Black will emit the entire input to stdout which is both excessively
# noisy and useless for errors, so discard it.
e.stdout = None
raise formatters.ParseError(path) from e
return _custom_format_data(result.stdout)