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