Source code for symforce.codegen.format_util
# ----------------------------------------------------------------------------
# SymForce - Copyright 2022, Skydio, Inc.
# This source code is under the Apache 2.0 license found in the LICENSE file.
# ----------------------------------------------------------------------------
import json
import os
import shutil
import subprocess
from pathlib import Path
from symforce import typing as T
from symforce.path_util import symforce_dir
from symforce.python_util import find_ruff_bin
[docs]def format_cpp(file_contents: str, filename: str) -> str:
"""
Autoformat a given C++ file using clang-format
Args:
file_contents: The unformatted contents of the file
filename: A name that this file might have on disk; this does not have to be a real path,
it's only used for clang-format to find the correct style file (by traversing upwards
from this location) and to decide if an include is the corresponding .h file for a .cc
file that's being formatted (this affects the include order)
Returns:
formatted_file_contents (str): The contents of the file after formatting
"""
try:
import clang_format
clang_format_path = str(
Path(clang_format.__file__).parent / "data" / "bin" / "clang-format"
)
except ImportError:
clang_format_path = "clang-format"
result = subprocess.run(
[clang_format_path, f"-assume-filename={filename}"],
input=file_contents,
stdout=subprocess.PIPE,
stderr=None,
check=True,
text=True,
)
return result.stdout
[docs]def format_py(file_contents: str, filename: str) -> str:
"""
Autoformat a given Python file using ruff
Args:
filename: A name that this file might have on disk; this does not have to be a real path,
it's only used for ruff to find the correct style file (by traversing upwards from this
location)
"""
result = subprocess.run(
[find_ruff_bin(), "format", f"--stdin-filename={filename}", "-"],
input=file_contents,
stdout=subprocess.PIPE,
check=True,
# Disable the ruff cache. This is important for running in a hermetic context like a bazel
# test, and shouldn't really hurt other use cases. If it does, we should work around this
# differently.
env=dict(os.environ, RUFF_NO_CACHE="true"),
text=True,
)
result = subprocess.run(
[
find_ruff_bin(),
"check",
"--select=I",
"--fix",
"--quiet",
f"--stdin-filename={filename}",
"-",
],
input=result.stdout,
stdout=subprocess.PIPE,
check=True,
# Disable the ruff cache. This is important for running in a hermetic context like a bazel
# test, and shouldn't really hurt other use cases. If it does, we should work around this
# differently.
env=dict(os.environ, RUFF_NO_CACHE="true"),
text=True,
)
return result.stdout
[docs]def format_py_dir(dirname: T.Openable) -> None:
"""
Autoformat python files in a directory (recursively) in-place
"""
subprocess.run(
[find_ruff_bin(), "format", dirname],
check=True,
# Disable the ruff cache. This is important for running in a hermetic context like a bazel
# test, and shouldn't really hurt other use cases. If it does, we should work around this
# differently.
env=dict(os.environ, RUFF_NO_CACHE="true"),
text=True,
)
_rustfmt_path: T.Optional[Path] = None
def _find_rustfmt() -> Path:
"""
Find the rustfmt binary
"""
global _rustfmt_path # noqa: PLW0603
if _rustfmt_path is not None:
return _rustfmt_path
rustfmt = shutil.which("rustfmt")
if rustfmt is None:
raise FileNotFoundError("Could not find rustfmt")
# Ignore the type because mypy can't reason about the fact that we just checked that rustfmt
# is not None.
_rustfmt_path = Path(rustfmt)
return _rustfmt_path
[docs]def format_rust(file_contents: str, filename: str) -> str:
"""
Autoformat a given Rust file using rustfmt.
Args:
filename: A name that this file might have on disk; this does not have to be a real path,
it's only used for ruff to find the correct style file (by traversing upwards from this
location)
"""
result = subprocess.run(
[_find_rustfmt()],
input=file_contents,
stdout=subprocess.PIPE,
check=True,
text=True,
)
return result.stdout
_prettier_cmd: T.Optional[T.List[str]] = None
def _get_prettier_cmd() -> T.List[str]:
"""
Returns a command to run prettier with any necessary args
"""
global _prettier_cmd # noqa: PLW0603
if _prettier_cmd is not None:
return _prettier_cmd
# A path to a local prettier binary can be specified in the paths.json file with they key
# "prettier_bin_path". The path should be relative to the symforce root directory.
# If not provided, we will use npx or yarn if installed.
paths_file = symforce_dir() / "build" / "paths.json"
if paths_file.exists():
with open(paths_file, "r") as f:
paths_data = json.load(f)
if prettier_path := paths_data.get("prettier_bin_path"):
_prettier_cmd = [symforce_dir() / prettier_path]
# NOTE(nathan): This is necessary when using aspect_rules_js to suppress the warning
# about BAZEL_BINDIR not being set. Otherwise it's unnecessary.
if "BAZEL_BINDIR" not in os.environ:
os.environ["BAZEL_BINDIR"] = "."
elif npx := shutil.which("npx"):
_prettier_cmd = [npx, "prettier"]
elif yarn := shutil.which("yarn"):
_prettier_cmd = [yarn, "prettier"]
if not _prettier_cmd:
raise FileNotFoundError("Could not find prettier")
return _prettier_cmd
[docs]def format_typescript(file_contents: str, config_path: Path) -> str:
"""
Autoformat a given TypeScript file using prettier.
"""
# NOTE(nathan): We would use the "--stdin-filepath" arg with a dummy file like we do for the
# other formatters, but there seems to be a prettier bug where it doesn't want to use the config
# file that is found by traversing upwards from the dummy file. Instead we just point to the
# config file directly, which seems to work.
result = subprocess.run(
[*_get_prettier_cmd(), "--config", config_path, "--parser", "typescript"],
input=file_contents,
stdout=subprocess.PIPE,
check=True,
text=True,
)
return result.stdout